Framework Architecture

Domain-centric Angular framework with model-driven architecture, auto-generated code, and NgRx state management

Angular 20+ Domain-Centric Model-Driven Code Generation NgRx

The Vision: Build Apps Faster

Tired of writing the same code over and over? Every Angular project needs forms, listings, API calls, and state management. CartesianUI changes the game: define your data model once, and the framework generates everything else automatically — forms, tables, API integration, and state management.

80%

Code Auto-Generated

Focus on What Matters

Stop wiring boilerplate. CartesianUI's domain-centric approach means you define your business entities once, and the framework handles the rest. More time solving real problems, less time on repetitive infrastructure.

Whether you're a business owner looking to build an application quickly, a startup that needs to move fast, or an enterprise team wanting consistency across projects — CartesianUI provides a battle-tested foundation that scales from prototype to production.

The Problem

  • Repeated boilerplate in every Angular app
  • Every feature needs: models, forms, lists, state, API
  • More time wiring infrastructure than solving business logic
  • NgRx is powerful but requires lots of code
  • Inconsistent patterns across team members

The Solution

  • Models as single source of truth
  • Auto code generation for complete CRUD
  • Built-in NgRx state management
  • Sandbox pattern keeps components thin
  • Consistent architecture everyone follows

High-Level Architecture

The application is organized as an Angular 20+ monorepo with multiple applications sharing feature libraries.

Admin App
Care App
POS App
admin-user
admin-auth
care-visit
care-queue
care-patient
@cartesianui/core
@cartesianui/common
@cartesianui/coreui

Think of it like building blocks: The core libraries provide the foundation (HTTP, state management, UI components). Feature libraries contain your business logic (users, patients, orders). Applications combine these libraries into complete products.

Models: The Foundation

Every domain starts with a model. Models extend from ParentModel and define not just data — but also validation, forms, and listings.

What makes CartesianUI models special? In traditional development, you define your data structure in one place, form validation in another, table columns somewhere else, and API mappings in yet another file. With CartesianUI, all of this lives in one model. Change it once, and everything updates automatically.

From Model to Full Feature

Rich Model
Forms
Listings
API
Store

Model Capabilities

  • JSON-to-object conversion
  • Reactive form integration
  • API mapping & normalization
  • Validation rules
  • UI schema (forms, tables, search)

Define Once, Use Everywhere

  • dataTableCols → table headers
  • formFields → forms + validators
  • searchForm → filters
  • Same model feeds Forms, API, Store, Listings

Basic Model Structure

export interface IAccount { id: string; name: string; balance: number; } export class Account extends ParentModel implements IAccount { id: string; name: string; balance: number; constructor(data?: IAccount) { super(data); } }

DataTable Columns

static override get dataTableCols(): FieldDescriptor[] { return [ { key: 'id', label: 'Id', opt: { link: true } }, { key: 'name', label: 'Name' }, { key: 'balance', label: 'Balance', opt: { formatter: { type: 'currency', locale: 'en-PK', currency: 'PKR' } } }, { key: 'openedAt', label: 'Opened At', opt: { formatter: { type: 'date', to: DateFormat.MED } } } ]; }

Form Fields with Validation

static override formFields: FieldDescriptor[] = [ { key: 'name', label: 'Customer Name', opt: { validators: [Validators.required, Validators.minLength(3)] } }, { key: 'email', label: 'Email', opt: { validators: [Validators.email] } }, { key: 'balance', label: 'Opening Balance', opt: { formatter: { type: 'currency' } } } ];

Search Form Criteria

static override get searchForm() { return { name: { column: 'name', operator: 'like', value: null }, email: { column: 'email', operator: '=', value: null }, status: { column: 'status', operator: '=', value: null } }; }

ParentModel Helper Methods

// Convert API response to typed object const customer = Customer.fromJson(apiResponse); // Convert object to API payload const payload = customer.toJson(); // Generate Angular FormGroup const formGroup = customer.toFormGroup(); // Create entity from form values const updated = Customer.fromFormGroup(formGroup);

Core Building Blocks

The framework provides six main building blocks that work together seamlessly.

Models

Define fields, validation, rules, listings, and forms. Single source of truth for your domain.

Forms

Auto-generated from model with validators. Client + server validation built in.

Listings

DataTable, pagination, search, and filters all derived from model configuration.

HTTP Services

Decorator-based API definitions with adapters for data normalization.

Sandbox

Facade pattern connecting components to store. Keeps UI components thin and focused.

State (NgRx)

Actions, Reducers, Effects, Selectors — auto-generated for each entity.

Data Flow Architecture

Unidirectional data flow using NgRx and Facade pattern ensures predictable state changes.

Component
Sandbox
Store
Effects
HTTP
API
Response
Effects
Reducer
Selectors
Component

How it works: When a user clicks a button, the Component calls the Sandbox. The Sandbox dispatches an Action to the Store. Effects listen for that action and call the HTTP service. When the API responds, Effects dispatch a success action that updates the state via the Reducer. Selectors expose that state back to the Component through the Sandbox.

Forms & Validation

Form building is one of the strongest features. Validators in the model automatically generate Angular Reactive Forms.

Without CartesianUI
  • Write FormGroup manually
  • Add Validators manually
  • Map back to DTO manually
  • Handle loading/errors manually
With CartesianUI
  • Extend FormBaseComponent<Account>
  • Init form from model fields
  • Save = this.sb.account.create(entity)
  • Busy, success, error → all handled

Form Component

export class AccountCreateComponent extends FormBaseComponent<Account> { constructor(injector: Injector, private sb: BookeeperSandbox) { super(injector, Account); this.initForm(); } onSave(): void { if (this.formGroup.valid) { const entity = this.getEntityFromForm(); this.sb.account.create(entity); } } }

Form Template

<default-actions (save)="onSave()" [disabled]="{ save: formGroup.invalid }"></default-actions> <form [formGroup]="formGroup"> <with-validation> <label>Name</label> <input validate required formControlName="name" class="form-control" /> </with-validation> </form>

Data Listings

Columns and search criteria are described in the model. Drop in components for a complete listing with pagination, sorting, and filtering.

Without CartesianUI
  • Custom service for filtering
  • Manually manage pagination, sorting
  • Wire up search + table events
  • Lots of repetitive code
With CartesianUI
  • Extend ListingControlsComponent<Account>
  • Use RequestCriteria for search, filters, pagination
  • this.sb.account.fetchAll(this.criteria)
  • DataTable auto-wires

Listing Component

export class AccountListingComponent extends ListingControlsComponent<IAccount> { constructor(injector: Injector, private sb: BookeeperSandbox) { super(injector); } ngOnInit() { this.initCriteria(); this.list(); } protected list() { this.sb.account.fetchAll(this.criteria); } }

Listing Template

<page-actions (searchChange)="onSearch($event)"> <button page-action="create">Add Account</button> </page-actions> <data-table [columns]="Account.dataTableCols" [rows]="sb.account.entities$ | async" (action)="onAction($event)"> </data-table> <search-form [config]="Account.searchForm" (search)="accountSandbox.search($event)"> </search-form>

State Management

Redux store auto-generated per domain. Actions, Reducers, Effects — minimal coding required.

Actions createEntityActions()
Reducer createEntityFeature()
Effects BaseEntityEffects
State EntityState + Extended
Selectors Auto-generated

Actions (One-liner)

const actions = createEntityActions<Account>('Account'); export const AccountActions = { ...actions }; // Includes: fetchAll, fetchById, create, update, delete, select...

Effects (Extend Base)

@Injectable() export class AccountEffects extends BaseEntityEffects<Account> { constructor(actions$: Actions, http: AccountHttpService) { super(http, AccountActions); } // CRUD effects ready out of the box }

Reducer (One-liner)

export const fromAccount = createEntityFeature<Account>('accounts', AccountActions); // Auto-generates: state, adapters, selectors (selectAll, selectEntities, selected...)

Entity State Structure

interface EntityStateExtended<T> extends EntityState<T> { meta: ResponseMeta | null; // Pagination info request: RequestState; // Fetch state create: RequestState; // Create state update: RequestState; // Update state delete: RequestState; // Delete state selected: T | null; // Selected entity } interface RequestState { started: boolean; completed: boolean; failed: boolean; status?: string; }

Extending State Entities

The generated state is just a starting point. Here's how to extend actions, effects, reducers, and selectors for custom business logic.

Extending Actions

Add custom actions alongside the generated ones:

// Start with generated CRUD actions const baseActions = createEntityActions<Account>('Account'); // Add custom actions export const AccountActions = { ...baseActions, // Custom action for approving an account approveAccount: createAction( '[Account] Approve Account', props<{ id: string }>() ), approveAccountSuccess: createAction( '[Account] Approve Account Success', props<{ account: Account }>() ), // Custom action for bulk operations archiveMultiple: createAction( '[Account] Archive Multiple', props<{ ids: string[] }>() ) };

Extending Effects

Override or add new effects for custom API calls:

@Injectable() export class AccountEffects extends BaseEntityEffects<Account> { constructor( actions$: Actions, http: AccountHttpService, private notify: NotifyService ) { super(http, AccountActions); } // Custom effect for account approval approveAccount$ = createEffect(() => this.actions$.pipe( ofType(AccountActions.approveAccount), switchMap(({ id }) => this.http.approve(id).pipe( map(account => AccountActions.approveAccountSuccess({ account })), catchError(error => of(AccountActions.fetchFailed({ error }))) ) ) ) ); // Override base effect to add notification override createSuccess$ = createEffect(() => this.actions$.pipe( ofType(AccountActions.createSuccess), tap(({ entity }) => { this.notify.success(`Account ${entity.name} created!`); }) ), { dispatch: false } ); }

Extending Reducers

Handle custom actions with additional reducer cases:

// Get base feature (includes reducer + selectors) const baseFeature = createEntityFeature<Account>('accounts', AccountActions); // Extend the reducer for custom actions export const accountReducer = createReducer( baseFeature.initialState, // Include all base cases ...baseFeature.reducerCases, // Add custom case for approval on(AccountActions.approveAccountSuccess, (state, { account }) => baseFeature.adapter.updateOne( { id: account.id, changes: { status: 'approved' } }, state ) ), // Add custom case for bulk archive on(AccountActions.archiveMultiple, (state, { ids }) => ({ ...state, update: { started: true, completed: false, failed: false } })) ); // Export feature with custom reducer export const fromAccount = { ...baseFeature, reducer: accountReducer };

Adding Custom Selectors

Create derived selectors for computed state:

// Base selectors from createEntityFeature const { selectAll, selectEntities, selectSelected } = fromAccount.selectors; // Custom selector: filter by status export const selectActiveAccounts = createSelector( selectAll, (accounts) => accounts.filter(a => a.status === 'active') ); // Custom selector: total balance export const selectTotalBalance = createSelector( selectAll, (accounts) => accounts.reduce((sum, a) => sum + a.balance, 0) ); // Custom selector: accounts by type export const selectAccountsByType = (type: string) => createSelector( selectAll, (accounts) => accounts.filter(a => a.type === type) ); // Combine selectors export const selectDashboardData = createSelector( selectActiveAccounts, selectTotalBalance, (accounts, total) => ({ accounts, total, count: accounts.length }) );

Adding Custom State Properties

Extend the state interface for domain-specific needs:

// Extend the base state interface AccountState extends EntityStateExtended<Account> { // Custom properties filterStatus: string | null; pendingApprovals: string[]; lastSyncedAt: Date | null; } // Initial state with custom properties const initialState: AccountState = { ...baseFeature.initialState, filterStatus: null, pendingApprovals: [], lastSyncedAt: null }; // Handle custom state in reducer on(AccountActions.setFilter, (state, { status }) => ({ ...state, filterStatus: status })), on(AccountActions.syncComplete, (state) => ({ ...state, lastSyncedAt: new Date() }))

Sandbox Pattern

Components don't talk directly to the store — they only call sandbox methods and subscribe to observables.

Why Sandbox? It's a facade that hides the complexity of NgRx from your components. Components just call simple methods like fetchAll() or create(entity). The sandbox handles dispatching actions and exposing state. This keeps your UI code clean and testable.

Benefits

  • Components stay thin (UI only)
  • Business logic centralized
  • Cleaner separation of concerns
  • Easier testing & scaling
  • Dispatches actions & exposes observables

EntitySandbox Features

  • entities$ / entities (Observable + Signal)
  • fetchAll(), fetchById(), create(), update(), delete()
  • Request lifecycle: busy, completed, error
  • Pagination & meta data
  • Clear state helpers

Sandbox Example

@Injectable({ providedIn: 'root' }) export class BookeeperSandbox extends Sandbox { // EntitySandbox wraps store access account = new EntitySandbox<Account>(this.store, AccountActions, fromAccount); constructor(private store: Store) { super(); } }

Usage in Component

// Component only interacts with Sandbox export class AccountListComponent { sb = inject(BookeeperSandbox); // Subscribe to entities (Observable or Signal) accounts$ = this.sb.account.entities$; accounts = this.sb.account.entities; // Signal ngOnInit() { this.sb.account.fetchAll(criteria); } onDelete(id: string) { this.sb.account.delete(id); } }

Service Types

HTTP Services

Decorator-based API definitions

  • @GET(), @POST(), @PATCH(), @DELETE()
  • @Path('id') for URL params
  • @Body for request body
  • @Criteria for query params
  • @DefaultHeaders() class decorator

Sandbox Services

Facade pattern for state access

  • Dispatches actions to store
  • Exposes selectors as observables
  • Exposes signals (modern pattern)
  • EntitySandbox for generic CRUD
  • Abstracts store complexity

Core Services

Shared infrastructure services

  • SessionService - User session
  • TokenService - JWT handling
  • LocalizationService - i18n
  • PermissionCheckerService - RBAC
  • MessageService - Notifications

UI Services

User interface utilities

  • NotifyService - Toasts/alerts
  • UiService - UI state
  • SettingService - App settings
  • HttpErrorService - Error handling
  • ValidationService - Form validation

HTTP Layer & Request Criteria

Unified HTTP service with decorators. Adapters for API normalization.

Decorator-Based HTTP Service

@Injectable() @DefaultHeaders({ 'Content-Type': 'application/json' }) export class AccountHttpService extends HttpService { @GET('/accounts') accounts(@Criteria criteria: RequestCriteriaOutput): Observable<any> { return null; // Decorator handles HTTP call } @POST('/accounts') create(@Body body: Account): Observable<any> { return null; } @PATCH('/accounts/{id}') update(@Path('id') id: string, @Body body: Partial<Account>): Observable<any> { return null; } @DELETE('/accounts/{id}') delete(@Path('id') id: string): Observable<any> { return null; } }

Request Criteria

const criteria = new RequestCriteria() .where('status', '=', 'active') .orderBy('name', 'asc') .page(1) .limit(20); // Auto-normalized to API query parameters: // { search: ["name:cash"], searchFields: ["name:like"], page: [1], limit: [20] }

Component Hierarchy

BaseComponent<TChild>
ListingControlsComponent<T>
FormBaseComponent<T>
UsersComponent
QueueListComponent
CreateUserComponent
EditVisitComponent

Component Types

Type Base Class Purpose
Smart / Container ListingControlsComponent Data listing, pagination, search, child modals
Form / Dumb FormBaseComponent Create/Edit forms, validation, events
Entry Point BaseComponent Feature route container with router-outlet
Presentational None (standalone) Pure UI display with @Input/@Output

Modern Signal-Based Pattern

@Component({ changeDetection: ChangeDetectionStrategy.OnPush, standalone: true }) export class ListingComponent { sb = inject(EntitySandbox); // React to state changes with effect() private readonly busyEffect = effect(() => { this.handleBusyState(this.sb.entity.getState()); }); // Access data via signals entities = this.sb.entity.entities; // Signal<T[]> pagination = this.sb.entity.pagination; // Signal<Pagination> }

Key Architectural Patterns

Facade Pattern (Sandbox)

Sandbox services abstract NgRx store complexity, providing a clean API for components.

// Component only interacts with Sandbox this.sb.fetchUsers(criteria); this.sb.users$.subscribe(users => ...);

Generic Entity Operations

EntitySandbox, entityActions(), and EntityEffect provide reusable CRUD abstractions.

// One-line action group generation const actions = entityActions<User>('User'); // Includes: fetchAll, create, update, delete...

Decorator-Based HTTP

Method decorators define HTTP endpoints declaratively (similar to Retrofit).

@GET('/users/{id}') getUser(@Path('id') id: string) {}

Lazy Loading with Providers

Feature modules export routes and use provideFeature() for DI configuration.

export function provideUserFeature() { return makeEnvironmentProviders([ importProvidersFrom( EffectsModule.forFeature([...]) ) ]); }

Signals + Observables Hybrid

EntitySandbox exposes both observables (backward compatible) and signals (modern).

// Observable (traditional) sb.entities$: Observable<T[]> // Signal (modern) sb.entities: Signal<T[]>

Typed Constants Pattern

Use const objects with 'as const' for type-safe enumerations with label generation.

export const Statuses = { PENDING: 'pending', ACTIVE: 'active' } as const; type Status = (typeof Statuses)[...]

Code Generator

Generate new library + module in seconds. 80-90% of boilerplate auto-generated.

This is where the magic happens. Instead of manually creating models, services, state, and components, simply run a command and get a complete, working CRUD feature. Modify the generated code to add your business logic — the foundation is already there.

Input

Library name + Entity names

Output

  • Domain models
  • Search models
  • HTTP Services
  • Store/state (actions, reducers, effects)
  • Sandbox
  • List + Form components
# Generate complete library with CRUD ng generate @cartesianui/ng-cli:library bookeeper # Add entity to existing library ng generate @cartesianui/ng-cli:entity account --library=bookeeper # Result: Complete CRUD in seconds # - Model with fields and validation # - Form and listing components # - Store setup with actions, reducers, effects # - Sandbox and HTTP service

Project Structure

ng-web/
apps/ Application entry points
admin/ Main admin portal
care/ Healthcare system
pos/ Point of sales
projects/ Feature libraries
shared/
core/ HTTP, Auth, Services
common/ Base classes, Store utils
coreui/ UI framework wrapper
admin/ Admin feature modules
care/ Care feature modules
angular.json Workspace config
tsconfig.json TypeScript config

Feature Module Structure

feature-module/src/lib/
models/
domain/ Entity interfaces & classes with metadata
store/ NgRx state management
actions.ts Auto-generated + custom actions
reducer.ts State reducer + selectors
effect.ts Side effects (HTTP calls)
shared/
http.service.ts Decorated API methods
ui/ Components
listing.component.ts Extends ListingControlsComponent
create/ Extends FormBaseComponent
edit/ Extends FormBaseComponent
[feature].sandbox.ts Facade service with EntitySandbox
[feature].providers.ts DI configuration
[feature].routes.ts Feature routing

Shared Libraries (@cartesianui)

@cartesianui/core

  • HttpService base class
  • HTTP decorators
  • CartesianHttpInterceptor
  • SessionService, TokenService
  • LocalizationService
  • PermissionCheckerService
  • RequestCriteria utilities

@cartesianui/common

  • BaseComponent, Sandbox
  • ListingControlsComponent
  • FormBaseComponent
  • EntitySandbox, EntityEffect
  • entityActions(), entityFeature()
  • Form controls
  • Validation directives

@cartesianui/coreui

  • BoLayoutModule
  • DefaultLayoutComponent
  • Sidebar, Header components
  • CoreUI theme integration

Feature Libraries

Domain Libraries Key Entities
Admin admin-user, admin-auth, admin-tenancy, admin-catalog, admin-procurement, admin-sales, admin-bookeeper User, Role, Permission, Tenant, Product, Category
Care care-appointment, care-patient, care-visit, care-queue, care-shift Appointment, Patient, Visit, QueueEntry, Shift, PrescriptionItem

Technology Stack

Feature Implementation
Change Detection Zoneless (provideZonelessChangeDetection)
Components Standalone with inject()
State Management NgRx with Entity Adapter
Routing Lazy loading with provideFeature()
Forms Reactive Forms with custom controls
HTTP Decorator-based service methods
Reactivity Signals + Observables hybrid
Multi-tenancy TenancyService with tenant context

Core Takeaways

Domain-Centric Architecture

Pragmatic approach where models are the backbone. Define fields, validation, UI schema in one place.

NgRx with Zero Boilerplate

State management almost fully generated. Actions, reducers, effects, selectors ready to use and extend.

Sandbox Keeps Components Clean

UI components stay thin and testable. All business logic centralized in sandbox services.

80-90% Code Generated

Code generator automates setup. Focus on the 10-20% business-specific logic.

Enterprise-Ready & Open Source

Consistent architecture that's easy to maintain and onboard new developers. API integration, pagination, filters, and state handling all built in. Generate your first entity and see CRUD in action within minutes.