Documentation

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

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

Why CartesianUI

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

How It's Organized

An Angular 20+ monorepo where multiple applications share feature libraries built on a common platform.

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 BaseModel and use the @EntityMeta decorator to define not just data — but also validation, forms, listings, and search criteria in one place.

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 via the @EntityMeta decorator. Change it once, and everything updates automatically.

From Model to Full Feature

@EntityMeta Model
Forms
Listings
API
Store

Model Capabilities

  • JSON-to-object conversion (fromJSON / toJSON)
  • Reactive form integration (toForm / fromForm)
  • API mapping & normalization
  • Validation rules via FieldDescriptor
  • UI schema (forms, tables, search) via @EntityMeta

Define Once, Use Everywhere

  • @EntityMeta.list → datatable columns & headers
  • @EntityMeta.form → form fields + validators
  • @EntityMeta.search → search criteria & filters
  • Same model feeds Forms, API, Store, Listings

Entity Model with @EntityMeta (Recommended)

import { Validators } from '@angular/forms'; import { BaseModel, EntityMeta, FieldDescriptor } from '@cartesianui/common'; export interface IProduct { id?: string; name: string; status?: string; } @EntityMeta({ list: [ { key: 'name', label: 'Name', opt: { link: true } }, { key: 'status', label: 'Status', opt: { formatter: { displayAs: 'badge', valueMap: { 'active': { label: 'Active', color: 'success' }, 'disabled': { label: 'Disabled', color: 'danger' } } } }} ], form: [ { key: 'name', label: 'Name', opt: { validators: [Validators.required] } }, { key: 'status', label: 'Status', opt: { validators: [Validators.required] } } ], search: { id: { column: 'id', operator: '=', value: null }, name: { column: 'name', operator: '=', value: null } } }) export class Product extends BaseModel implements IProduct { id?: string; name: string; status?: string; }

Option Values Pattern (Type-Safe Constants)

For fields with fixed values (status, type), define typed constants with label generation:

// 1. Define const object with allowed values export const ProductStatuses = { DRAFT: 'draft', ACTIVE: 'active', DISABLED: 'disabled' } as const; // 2. Derive type from it export type ProductStatus = (typeof ProductStatuses)[keyof typeof ProductStatuses]; // 3. Add option getter on the model for dropdowns static getStatusOptions(): { name: string; value: ProductStatus }[] { return Object.entries(ProductStatuses).map(([key, value]) => ({ name: toLabel(key), // 'DRAFT' → 'Draft', 'ACTIVE' → 'Active' value: value as ProductStatus })); } // 4. Type-safe usage in code if (product.status === ProductStatuses.ACTIVE) { ... }

Formatter Examples

// Date formatting { key: 'createdAt', label: 'Created', opt: { formatter: { type: 'date', to: DateFormat.SHORT } }} // Currency { key: 'price', label: 'Price', opt: { formatter: { type: 'currency', currency: 'USD' } }} // Pattern (combine multiple fields into one column) { key: 'id', label: 'Name', opt: { link: true, formatter: { type: 'pattern', pattern: 'firstName lastName' } }} // Multiline (multiple fields in one cell) { key: 'name', label: 'Product', opt: { formatter: { type: 'multiline', separator: 'br', items: [ { key: 'name', displayAs: 'text' }, { key: 'genericName', displayAs: 'muted' } ] } }} // Badge with ValueMap { key: 'status', label: 'Status', opt: { formatter: { displayAs: 'badge', valueMap: { 'active': { label: 'Active', color: 'success' }, 'disabled': { label: 'Disabled', color: 'danger' } } } }}

BaseModel Helper Methods

// Convert API response to typed object const product = new Product(apiResponse); // init() auto-called // Generate Angular FormGroup from model metadata const formGroup = product.toForm(); // Populate model from FormGroup values const updated = product.fromForm(formGroup); // Format values for API submission const payload = product.dbFormatted(); // Format values for datatable display const cellValue = product.dtFormatted(column);

Core Building Blocks

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

Models

Define fields, validation, rules, listings, and forms via @EntityMeta. Single source of truth.

Forms

Auto-generated from model with validators. Client + server validation. Reusable controls.

Listings

AppDatatableComponent wrapper. Pagination, search, and filters derived from model config.

HTTP Services

Decorator-based API definitions (@GET, @POST, @PATCH, @DELETE) with adapters.

Sandbox

Facade with EntitySandbox. Signals + Observables. Keeps UI components thin.

State (NgRx)

entityActions(), entityFeature(), EntityEffect — auto-generated per entity.

Forms & Validation

Form building is one of the strongest features. Model metadata automatically generates Angular Reactive Forms via FormBaseComponent.

Without CartesianUI
  • Write FormGroup manually
  • Add Validators manually
  • Map back to DTO manually
  • Handle loading/errors manually
With CartesianUI
  • Extend FormBaseComponent<Product>
  • initForm() reads @EntityMeta validators
  • getEntityFromForm() maps back automatically
  • Signal-based request state tracking

Create Form Component

@Component({ selector: 'admin-product-create', templateUrl: 'create.component.html', imports: [...FORM_IMPORTS], providers: [{ provide: ENTITY_CONSTRUCTOR, useValue: Product }], standalone: true }) export class ProductCreateComponent extends FormBaseComponent<Product> { sb = inject(CatalogSandbox); constructor() { super(Product); this.initForm(); // Creates FormGroup from @EntityMeta.form } // React to create completion via signals private readonly createEffect = effect(() => { if (this.sb.product.createCompleted()) { // Navigate away or show success } if (this.sb.product.createFailed()) { // Show error } }); onSave(): void { if (!this.formGroup.valid) return; const entity = this.getEntityFromForm(); this.sb.product.create(entity); } }

Edit Form Component

export class ProductEditComponent extends FormBaseComponent<Product> { sb = inject(CatalogSandbox); constructor() { super(Product); this.initForm(); } // Watch selected entity via signal, patch form when ready private readonly selectEffect = effect(() => { const selected = this.sb.product.selected(); if (!selected) return; const formValues = this.getFormFromEntity(selected); this.formGroup.patchValue(formValues.value); }); onSave(): void { const entity = this.getEntityFromForm(); this.sb.product.update(this.sb.product.selected()?.id, entity); } }

Form Template

<default-actions (save)="onSave()" [disabled]="{ save: formGroup.invalid }"></default-actions> <form [formGroup]="formGroup"> <with-validation [controlName]="'Name'"> <input validate formControlName="name" class="form-control" /> </with-validation> <!-- Searchable dropdown --> <selectable-control formControlName="categoryId" [options]="categories" optionKey="id" optionField="name" placeholder="Select category"> </selectable-control> <!-- Radio/checkbox grid from typed constants --> <choosable-control formControlName="type" [options]="Product.getTypeOptions()" optionKey="value" optionField="name"> </choosable-control> </form>

Reusable Form Controls

Control Description Usage
selectable-control Searchable dropdown (local + remote API) Category selection, vendor lookup
choosable-control Radio/checkbox grid from options array Product type, status selection
boolean-control Yes/No radio buttons Is active, manage inventory
switch-control Bootstrap toggle switch Track inventory, enable feature
file-uploader Drag-drop file upload with S3 support Product images, documents
repeatable-form Dynamic array of sub-forms Order line items, prescription items

Data Listings

Columns and search criteria come from @EntityMeta. The AppDatatableComponent wrapper replaces ~50 lines of raw ngx-datatable boilerplate with ~10 lines.

Without CartesianUI
  • Custom service for filtering
  • Manually manage pagination, sorting
  • Wire up search + table events
  • 50+ lines of ngx-datatable boilerplate
With CartesianUI
  • Extend ListingControlsComponent<T>
  • loadEntityMetadata() reads columns from model
  • <app-datatable> handles all rendering
  • Signal-based data binding (no | async)

Listing Component

@Component({ selector: 'admin-product-list', templateUrl: 'listing.component.html', imports: [...LISTING_IMPORTS], providers: [{ provide: ENTITY_CONSTRUCTOR, useValue: Product }], standalone: true }) export class ProductListingComponent extends ListingControlsComponent<IProduct> implements OnInit { sb = inject(CatalogSandbox); ngOnInit(): void { this.loadEntityMetadata(); // auto-reads columns, headers, searchForm this.initCriteria(); // reactive criteria, auto-calls list() on change } protected list(): void { this.sb.product.fetchAll(this.criteria.httpParams()); } }

Listing Template with AppDatatableComponent

<page-actions (searchChange)="onSearch($event)"> <button page-action="create">Add Product</button> </page-actions> <!-- ~10 lines instead of ~50 lines of raw ngx-datatable --> <app-datatable [rows]="sb.product.entities()" [columns]="columns" [headers]="headers" [count]="sb.product.pagination()?.total" [offset]="getOffsetFromPagination()" [limit]="sb.product.pagination()?.perPage" [selected]="selected" (selectChange)="onSelect($event)" (pageChange)="setPage($event)" (editClick)="onEdit($event)"> </app-datatable>

Custom Column Overrides & Extra Columns

Use the dtColumn directive to override cell rendering or add extra columns (like action buttons):

<app-datatable [rows]="..." [columns]="columns" [headers]="headers" ...> <!-- Override: custom component for the 'status' cell --> <ng-template dtColumn="status" let-row="row"> <my-status-badge [status]="row.status"></my-status-badge> </ng-template> <!-- Extra: actions column (__ prefix = new column) --> <ng-template dtColumn="__actions" columnName="Actions" [columnWidth]="120" let-row="row"> <button class="btn btn-sm btn-primary" (click)="onEdit(row)">Edit</button> <button class="btn btn-sm btn-danger" (click)="onDelete(row)">Delete</button> </ng-template> </app-datatable>

Row Detail (Expandable Rows)

Pass a detail template to enable expandable row content. Used by Purchase Orders, Delivery Notes, etc:

<app-datatable #dt [rows]="sb.purchaseOrder.entities()" [detailTemplate]="poDetail" ...> </app-datatable> <ng-template #poDetail let-row="row"> <div class="p-2"> <admin-purchase-order-item-list [rows]="row.items"> </admin-purchase-order-item-list> </div> </ng-template>

Cell Rendering Priority

Priority Condition Rendering
1 Custom template via dtColumn Content-projected ng-template
2 col.opt.link === true <a> tag with (click) → editClick
3 Formatter has displayAs or type: 'multiline' HTML via [innerHTML] (badges, tags, multiline)
4 Default Plain text via {{ row.dtFormatted(col) }}

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 — as both Signals and Observables.

State Management

NgRx store auto-generated per entity using factory utilities. Three files per entity — actions, reducer, effects.

Actions entityActions()
Reducer + Selectors entityFeature()
Effects EntityEffect
State EntityStateExtended
EntitySandbox Signals + Observables

1. Actions (One-liner)

import { entityActions } from '@cartesianui/common'; import { Product } from '../../models'; const actions = entityActions<Product, 'Product'>('Product'); export const ProductActions = { ...actions }; // Generated: fetchAll, fetchById, load, create, createSuccess, createFailure, // update, updateSuccess, updateFailure, delete, select, clear, ...

2. Reducer + Selectors (One-liner)

import { entityFeature } from '@cartesianui/common'; export const fromProduct = entityFeature<Product>('products', ProductActions); // Generates: entity adapter, reducer (handles all CRUD actions), // selectors (selectAll, selectEntities, selected, meta, request states)

3. Effects (Extend Base)

import { EntityEffect } from '@cartesianui/common'; @Injectable() export class ProductEffects extends EntityEffect<Product> { constructor(httpService: ProductHttpService) { super(httpService, ProductActions); } // CRUD effects ready out of the box: // fetchAll$, fetchById$, create$, update$, delete$ }

4. Wire Up in Providers

export function provideCatalogFeature(): EnvironmentProviders { return makeEnvironmentProviders([ importProvidersFrom( StoreModule.forFeature(fromProduct.featureKey, fromProduct.reducer), EffectsModule.forFeature([ProductEffects]), ), ProductHttpService, ]); }

Entity State Structure

interface EntityStateExtended<T> extends NgRxEntityState<T> { meta: ResponseMeta | null; // Pagination info request: RequestState; // Fetch state (started/completed/failed) get: RequestState; // Get-by-ID state create: RequestState; // Create state update: RequestState; // Update state delete: RequestState; // Delete state }

Extending Beyond CRUD

The generated CRUD is just the starting point. When an entity needs custom operations (e.g. getActiveShifts, endShift, checkout), extend all five layers following a consistent pattern — or use cui extend entity to auto-generate the boilerplate.

CLI Automation Available

Instead of writing extensions manually, use cui extend entity -l care -e Visit -a getOpenVisits to auto-generate the full 5-layer chain. See the Code Generator section for details.

The 5-Layer Extension Pattern: Every custom operation flows through the same layers as CRUD. This ensures consistent state tracking, error handling, and component integration. All five layers must be extended together. The sections below explain what each layer does — useful for understanding the generated code or for manual customization.

Extension Flow

1. HTTP Service
2. Actions
3. Reducer
4. Effects
5. Sandbox

Extension Checklist

When adding a custom operation (e.g. "End Shift"), touch all five files:

# File What to Add
1 shared/http.service.ts Method signature in IXxxHttpServiceExtension + decorated method
2 store/xxx/actions.ts Action triplet in additionalActions (action / success / failure)
3 store/xxx/reducer.ts Wrap reducer if custom state changes needed
4 store/xxx/effect.ts createEffect() wiring action → HTTP → success/failure
5 xxx.sandbox.ts dispatch() method for components to call

Step 1: HTTP Service — Add Custom Methods

Define an extension type declaring custom method signatures, then implement them:

// Extension type — declares custom methods beyond CRUD export type IShiftHttpServiceExtension = { getActiveShifts: (id: string) => Observable<ICartesianResponse>; endShift: (id: string) => Observable<ICartesianResponse>; }; @Injectable() export class ShiftHttpService extends HttpService implements IHttpService<Shift, IShiftHttpServiceExtension> { // Standard CRUD methods (getAll, getById, create, update, delete) ... // Extension methods @GET('/employees/{id}/active-shifts') public getActiveShifts(@Path('id') id: string): Observable<any> { return null; } @PATCH('/shifts/{id}/end') public endShift(@Path('id') id: string): Observable<any> { return null; } }

Step 2: Actions — Add Custom Actions

Spread base actions with additionalActions containing action triplets:

const actions = entityActions<Shift, 'Shift'>('Shift'); const additionalActions = { fetchActiveShifts: createAction('[Shift] Fetch Active Shift', props<{ id: string }>()), fetchActiveShiftsSuccess: createAction('[Shift] Fetch Active Shift Success', props<{ entity: Shift }>()), fetchActiveShiftsFailure: createAction('[Shift] Fetch Active Shift Failure', props<{ message: string; errors?: any }>()), endShift: createAction('[Shift] End Shift', props<{ id: string }>()), endShiftSuccess: createAction('[Shift] End Shift Success', props<{ entity: Shift }>()), endShiftFailure: createAction('[Shift] End Shift Failure', props<{ message: string; errors?: any }>()), }; // Merge base + custom export const ShiftActions = { ...actions, ...additionalActions };

Step 3: Reducer — Wrapper Reducer

Wrap the base reducer to handle custom actions. Key pattern: { ...baseFeature, reducer: customReducer } preserves all selectors:

const baseFeature = entityFeature<Shift>('shifts', ShiftActions); // Wrap reducer to handle custom actions const originalReducer = baseFeature.reducer; const customReducer = (state: any, action: any) => { let newState = originalReducer(state, action); if (action.type === ShiftActions.endShiftSuccess.type) { return { ...newState, selected: null }; // Clear selected after ending shift } return newState; }; // Replace only the reducer, keep all selectors export const fromShift = { ...baseFeature, reducer: customReducer };

Step 4: Effects — Custom createEffect

Pass the HTTP extension type as second generic to enable custom methods on this.httpService:

@Injectable() export class ShiftEffects extends EntityEffect<Shift, IShiftHttpServiceExtension> { constructor(actions$: Actions, httpService: ShiftHttpService) { super(httpService, ShiftActions); } // Custom effect — fetch active shifts for an employee fetchActiveShifts$ = createEffect(() => this.actions$.pipe( ofType(ShiftActions.fetchActiveShifts), switchMap(({ id }) => this.httpService.getActiveShifts(id).pipe( switchMap(({ data }) => data ? of(ShiftActions.select({ entity: data })) : EMPTY), catchError(({ message, errors }) => of(ShiftActions.fetchActiveShiftsFailure({ message, errors }))) ) ) ) ); // Custom effect — end a shift endShift$ = createEffect(() => this.actions$.pipe( ofType(ShiftActions.endShift), switchMap(({ id }) => this.httpService.endShift(id).pipe( map(({ data }) => ShiftActions.endShiftSuccess({ entity: data })), catchError(({ message, errors }) => of(ShiftActions.endShiftFailure({ message, errors }))) ) ) ) ); }

Step 5: Sandbox — Custom Dispatch Methods

Add methods that dispatch custom actions. Standard CRUD goes through sb.shift.fetchAll(), custom ops get their own methods:

@Injectable() export class ShiftSandbox extends Sandbox { private store = inject(Store); shift = new EntitySandbox<Shift>(this.store, this.injector, { selectors: fromShift, actions: ShiftActions, model: Shift, }); // Custom dispatch methods getActiveShifts(employeeId: string): void { this.store.dispatch(ShiftActions.fetchActiveShifts({ id: employeeId })); } endShift(shiftId: string): void { this.store.dispatch(ShiftActions.endShift({ id: shiftId })); } } // Usage in component: // Standard CRUD: sb.shift.fetchAll(), sb.shift.create(entity), sb.shift.selected() // Custom ops: sb.getActiveShifts(id), sb.endShift(id)

Extending Entity State

When custom operations need their own request tracking or additional state properties beyond standard CRUD, extend the entity state using the entityFeature() third parameter. Here is what the base state provides out of the box:

// Base state — every entity gets this automatically from entityFeature() interface EntityStateExtended<T> extends NgRxEntityState<T> { // NgRxEntityState provides: ids[], entities{} meta: ResponseMeta | null; // Pagination info from API selected: T | null; // Currently selected entity request: RequestState | undefined; // fetchAll state (started/completed/failed) get: RequestState | undefined; // fetchById state create: RequestState | undefined; // create state update: RequestState | undefined; // update state delete: RequestState | undefined; // delete state }

When you need additional properties — a separate request tracker, a "current" entity, or a secondary collection — pass an extension interface as the third argument:

// 1. Define extension interface with custom state properties export interface ICashDrawerStateExtended { currentDrawerRequest: RequestState | undefined; currentDrawer: CashDrawer | null; } // 2. Create initial values const stateExtension: ICashDrawerStateExtended = { currentDrawerRequest: requestDefault, currentDrawer: null }; // 3. Pass as third arg to entityFeature const baseFeature = entityFeature<CashDrawer, ICashDrawerStateExtended>( 'cashDrawers', CashDrawerActions, stateExtension ); // 4. Wrap reducer to handle custom actions const originalReducer = baseFeature.reducer; const customReducer = (state: any, action: any) => { let newState = originalReducer(state, action); switch (action.type) { case CashDrawerActions.fetchCurrentDrawer.type: return { ...newState, currentDrawerRequest: { ...requestStarted } }; case CashDrawerActions.fetchCurrentDrawerSuccess.type: return { ...newState, currentDrawer: action.entity, currentDrawerRequest: { ...requestCompleted } }; case CashDrawerActions.clearCurrentDrawer.type: return { ...newState, currentDrawer: null }; default: return newState; } }; // 5. Create custom selectors for extended properties const stateSelector = (baseFeature as any).selectCashDrawersState; const currentDrawer = createSelector(stateSelector, (state: any) => state.currentDrawer); const currentDrawerRequest = createSelector(stateSelector, (state: any) => state.currentDrawerRequest); // 6. Export feature with custom reducer + selectors export const fromCashDrawer = { ...baseFeature, reducer: customReducer, currentDrawer, currentDrawerRequest };

Then consume custom selectors in the sandbox via toSignal():

// In sandbox — expose extended state as signals currentDrawer = toSignal(this.store.select(fromCashDrawer.currentDrawer)); currentDrawerRequest = toSignal(this.store.select(fromCashDrawer.currentDrawerRequest));

When to Extend State

Scenario What to Add Example
Custom operation needs loading/error tracking Add RequestState property fetchMyCartsRequest, getOpenVisitsRequest
"Current" entity separate from selected Add typed property currentDrawer: CashDrawer | null
Separate entity collection ("my items" vs "all") Add array property entitiesAssignedToMe: QueueEntry[]
Derived computed data from custom state Add createSelector() chains selectQueueForDoctor, selectActiveAssignments

Sandbox Pattern

Components don't talk directly to the store — they call sandbox methods and read signals.

Why Sandbox? It's a facade that hides NgRx complexity from your components. Components call fetchAll() or create(entity), and read state via Angular Signals. The sandbox handles all dispatching and state exposure.

EntitySandbox Signals

  • entities() — Signal<T[]>
  • selected() — Signal<T>
  • pagination() — Signal<Pagination>
  • hasEntities() — Signal<boolean>
  • createCompleted() / createFailed()
  • updateCompleted() / updateFailed()
  • deleteCompleted() / deleteFailed()

EntitySandbox Methods

  • fetchAll(criteria) — fetch list
  • fetchById(id) — fetch + select
  • create(entity) — dispatch create
  • update(id, entity) — dispatch update
  • delete(id) — dispatch delete
  • select(entity) — set selected
  • clearRequestState(type) — reset state

Sandbox Setup

@Injectable({ providedIn: 'root' }) export class CatalogSandbox extends Sandbox { private store = inject(Store); product = new EntitySandbox<Product>(this.store, this.injector, { selectors: fromProduct, actions: ProductActions, model: Product }); category = new EntitySandbox<Category>(this.store, this.injector, { selectors: fromCategory, actions: CategoryActions, model: Category }); }

Usage in Component

export class ProductListComponent { sb = inject(CatalogSandbox); // Access data via signals (modern) entities = this.sb.product.entities; // Signal<Product[]> pagination = this.sb.product.pagination; // Signal<Pagination> // Or via observables (backward compatible) entities$ = this.sb.product.entities$; // Observable<Product[]> ngOnInit() { this.sb.product.fetchAll(criteria); } onDelete(id: string) { this.sb.product.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
  • IHttpServiceExtension for custom methods

Sandbox Services

Facade pattern for state access

  • EntitySandbox for generic CRUD
  • Signal properties (entities, selected, pagination)
  • Observable counterparts (entities$, selected$)
  • Custom dispatch methods for non-CRUD ops
  • Abstracts NgRx 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

Decorator-based HTTP service. Standard CRUD methods are auto-wired; custom methods use IHttpServiceExtension.

Decorator-Based HTTP Service

@Injectable() @DefaultHeaders({ 'Content-Type': 'application/json' }) export class ProductHttpService extends HttpService implements IHttpService<Product, IProductHttpServiceExtension> { @GET('/products') getAll(@Criteria criteria: RequestCriteriaOutput): Observable<any> { return null; } @GET('/products/{id}') getById(@Path('id') id: string): Observable<any> { return null; } @POST('/products') create(@Body body: Product): Observable<any> { return null; } @PATCH('/products/{id}') update(@Path('id') id: string, @Body body: Partial<Product>): Observable<any> { return null; } @DELETE('/products/{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

Component Hierarchy

BaseComponent<TChild>
ListingControlsComponent<T>
FormBaseComponent<T>
ProductListingComponent
QueueListComponent
ProductCreateComponent
ProductEditComponent

Component Types

Type Base Class Purpose
Listing ListingControlsComponent Data listing, pagination, search, filtering
Create Form FormBaseComponent Empty form, validates, dispatches create
Edit Form FormBaseComponent Patches from selected entity, dispatches update
Entry Point BaseComponent Feature route container with router-outlet
Presentational None (standalone) Pure UI display with @Input/@Output

Key Architectural Patterns

@EntityMeta Decorator

Single decorator defines list, form, and search metadata. The model IS the schema.

@EntityMeta({ list: [...], form: [...], search: {...} })

Factory Utilities

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

const actions = entityActions<T>('Name'); const feature = entityFeature<T>('key', actions);

Signals + Observables Hybrid

EntitySandbox exposes both. Use signals in new code, observables for backward compat.

// Signal (modern) sb.product.entities() // Observable (legacy) sb.product.entities$

5-Layer Extension Pattern

HTTP → Actions → Reducer → Effects → Sandbox. All five layers, every time.

IXxxHttpServiceExtension additionalActions { ...actions } { ...baseFeature, reducer } EntityEffect<T, IExtension>

Typed Constants Pattern

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

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

ENTITY_CONSTRUCTOR Token

Provide the model class via DI. ListingControls and FormBase auto-read metadata.

providers: [ { provide: ENTITY_CONSTRUCTOR, useValue: Product } ]

Code Generator (cui CLI)

Generate CRUD libraries in seconds, then extend with custom actions. 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. Need custom operations beyond CRUD? The extend entity command auto-generates the full 5-layer extension chain. Modify the generated code to add your business logic — the foundation is already there.

Create — CRUD Scaffold

Library name + Entity names → Complete CRUD

  • Domain models with @EntityMeta
  • HTTP Services with IHttpServiceExtension
  • Store (actions, reducers, effects)
  • Sandbox with EntitySandbox
  • List + Form components
  • Routes + Providers

Extend — Beyond CRUD

Entity + Action name → Full extension chain

  • HTTP method + IHttpServiceExtension entry
  • Action triplet (action / success / failure)
  • Custom reducer state + switch cases + selector
  • createEffect() wired to HTTP + actions
  • Sandbox observable + signal + dispatch

CRUD Generation Commands

# Create a new library with entities cui create library --lib=Catalog --entities=Product,Category # Short flags cui create library -l Catalog -e Product,Category -s admin # Add entities to existing library cui add entity --lib=Catalog --entities=Brand # Update model metadata from interface cui update model --lib=Catalog --entities=Product # Update form HTML template cui update form --lib=Catalog --entities=Product # Result: Complete CRUD in seconds # - Model with @EntityMeta decorator # - HTTP service with IHttpServiceExtension # - Store: actions, reducer (entityFeature), effects (EntityEffect) # - Sandbox with EntitySandbox # - Listing + Create/Edit components # - Routes and providers

Extend Entity — Custom Action Generation

After CRUD generation, use cui extend entity to add custom operations. Each call scaffolds a single action across all five layers. Run multiple times to add more actions to the same entity.

# Add a custom action to an existing entity cui extend entity --lib=care --entity=Visit --action=getOpenVisits # Short flags cui extend entity -l care -e Visit -a getOpenVisits # Add another action to the same entity (append mode) cui extend entity -l care -e Visit -a getVisitPrescription # With custom destination path cui extend entity -l care -e Visit -a getOpenVisits --dest=ng-web/projects/care/visit

Two Transformation Modes

The command auto-detects which mode to use based on whether the entity has been extended before:

Mode When What Happens
First Extension Entity has never been extended (no additionalActions in actions.ts) Restructures files: creates additionalActions, replaces entityFeature() export with baseFeature + custom reducer + selectors, adds 2nd type param to effect/http classes
Subsequent Extension Entity already has extensions Appends: new entries to existing additionalActions, interface, switch/case, selectors, dispatch methods

What Gets Generated (per action)

For cui extend entity -l care -e Visit -a getOpenVisits, the CLI modifies these 5 files:

# File Generated Code
1 shared/visit/http.service.ts getOpenVisits method signature in IVisitHttpServiceExtension + @GET('/visits/{id}/open-visits') decorated method
2 store/visit/actions.ts getOpenVisits / getOpenVisitsSuccess / getOpenVisitsFailure action triplet in additionalActions
3 store/visit/reducer.ts getOpenVisitsRequest: RequestState in state interface, requestDefault initial value, 3 switch cases, createSelector, export entry
4 store/visit/effect.ts getOpenVisits$ effect: createEffect() wiring action → httpService.getOpenVisits(id) → success/failure
5 [library].sandbox.ts getOpenVisitsRequest$ observable, getOpenVisitsRequest signal via toSignal(), getOpenVisits(id) dispatch method

Example: First Extension Output

Before extend (clean CRUD template) vs after running cui extend entity -l Catalog -e Product -a fetchRelated:

actions.ts — Before

const actions = entityActions<Product, 'Product'>('Product'); export const ProductActions = { ...actions, };

actions.ts — After

const actions = entityActions<Product, 'Product'>('Product'); const additionalActions = { fetchRelated: createAction('[Product] Fetch Related', props<{ id: string }>()), fetchRelatedSuccess: createAction(...), fetchRelatedFailure: createAction(...), }; export const ProductActions = { ...actions, ...additionalActions, };

reducer.ts — Before

export const fromProduct = entityFeature<Product>( 'products', ProductActions );

reducer.ts — After

interface IProductStateExtended { fetchRelatedRequest: RequestState; } const baseFeature = entityFeature< Product, IProductStateExtended >('products', ProductActions, stateExtension); const customReducer = (state, action) => { // switch on action.type... }; export const fromProduct = { ...baseFeature, reducer: customReducer, fetchRelatedRequest // selector };

Project Structure

ng-web/
apps/ Application entry points
admin/ Main admin portal
care/ Healthcare system
pos/ Point of sales
projects/ Feature libraries
platform/
core/ HTTP, Auth, Services
common/ Base classes, Store utils, Models, Forms, Datatable
coreui/ UI framework wrapper
shared/ Cross-app entities (Customer, Vendor, Location)
shopifier/ Commerce entities (Product, Order, Inventory)
care/ Healthcare entities (Visit, Queue, Patient)
pos/ POS entities (Cart, Drawer, Workspace)
angular.json Workspace config

Feature Library Structure (per entity)

feature-module/src/lib/
models/
domain/ Entity interfaces & @EntityMeta classes
store/ NgRx state management
product/ Folder-per-entity convention
actions.ts entityActions() + additionalActions
reducer.ts entityFeature() + wrapper if needed
effect.ts EntityEffect + custom createEffect()
shared/
product/ Per-entity shared
http.service.ts IHttpService + IHttpServiceExtension
ui/ Components
listing.component.ts Extends ListingControlsComponent
create/ Extends FormBaseComponent
edit/ Extends FormBaseComponent
[feature].sandbox.ts Sandbox with EntitySandbox per entity
[feature].providers.ts StoreModule.forFeature + EffectsModule.forFeature
[feature].routes.ts Feature routing

Shared Libraries (@cartesianui)

@cartesianui/core

  • HttpService base class
  • HTTP decorators (@GET, @POST...)
  • CartesianHttpInterceptor
  • SessionService, TokenService
  • LocalizationService
  • PermissionCheckerService
  • RequestCriteria utilities

@cartesianui/common

  • BaseModel, @EntityMeta decorator
  • ListingControlsComponent
  • AppDatatableComponent
  • FormBaseComponent
  • EntitySandbox, EntityEffect
  • entityActions(), entityFeature()
  • Form controls & validation

@cartesianui/coreui

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

Feature Libraries

Domain Libraries Key Entities
Shopifier catalog, procurement, sales, inventory Product, ProductSku, PurchaseOrder, SalesOrder, DeliveryNote, Inventory
Shared location, customer, vendor, employee, category, shift Country, City, State, Customer, Vendor, Employee, Shift
Care appointment, patient, visit, queue Appointment, Patient, Visit, QueueEntry, Prescription
POS cart, drawer, workspace, catalog Cart, CartItem, CashDrawer, Workspace
Accounting bookeeper Account, JournalVoucher

Technology Stack

Feature Implementation
Change Detection Zoneless (provideZonelessChangeDetection)
Components Standalone with inject()
State Management NgRx with Entity Adapter + factory utilities
Routing Lazy loading with provideFeature()
Forms Reactive Forms with FormBaseComponent + reusable controls
HTTP Decorator-based methods with IHttpServiceExtension
Reactivity Signals + Observables hybrid (EntitySandbox)
Multi-tenancy TenancyService with tenant context
Code Generation cui CLI (cui create library, cui add entity, cui update model)

Core Takeaways

Model-Driven with @EntityMeta

Define list columns, form fields, search criteria, and formatters in one decorator. The model IS the schema.

NgRx with Zero Boilerplate

entityActions(), entityFeature(), EntityEffect — three one-liners per entity. Extend with the 5-layer pattern.

Sandbox Keeps Components Clean

EntitySandbox provides Signals and Observables. Components stay thin — just call methods and read signals.

80-90% Code Generated

cui CLI generates complete CRUD features. Focus on the 10-20% business-specific logic like custom operations and formatters.

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. Need custom operations? Run cui extend entity to auto-scaffold the full 5-layer extension chain.