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.
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
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:
export const ProductStatuses = {
DRAFT: 'draft',
ACTIVE: 'active',
DISABLED: 'disabled'
} as const;
export type ProductStatus = (typeof ProductStatuses)[keyof typeof ProductStatuses];
static getStatusOptions(): { name: string; value: ProductStatus }[] {
return Object.entries(ProductStatuses).map(([key, value]) => ({
name: toLabel(key),
value: value as ProductStatus
}));
}
if (product.status === ProductStatuses.ACTIVE) { ... }
Formatter Examples
{ key: 'createdAt', label: 'Created', opt: {
formatter: { type: 'date', to: DateFormat.SHORT }
}}
{ key: 'price', label: 'Price', opt: {
formatter: { type: 'currency', currency: 'USD' }
}}
{ key: 'id', label: 'Name', opt: {
link: true,
formatter: { type: 'pattern', pattern: 'firstName lastName' }
}}
{ key: 'name', label: 'Product', opt: {
formatter: {
type: 'multiline', separator: 'br',
items: [
{ key: 'name', displayAs: 'text' },
{ key: 'genericName', displayAs: 'muted' }
]
}
}}
{ key: 'status', label: 'Status', opt: {
formatter: {
displayAs: 'badge',
valueMap: {
'active': { label: 'Active', color: 'success' },
'disabled': { label: 'Disabled', color: 'danger' }
}
}
}}
BaseModel Helper Methods
const product = new Product(apiResponse);
const formGroup = product.toForm();
const updated = product.fromForm(formGroup);
const payload = product.dbFormatted();
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.
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();
this.initCriteria();
}
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>
<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" ...>
<ng-template dtColumn="status" let-row="row">
<my-status-badge [status]="row.status"></my-status-badge>
</ng-template>
<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.
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 };
2. Reducer + Selectors (One-liner)
import { entityFeature } from '@cartesianui/common';
export const fromProduct = entityFeature<Product>('products', ProductActions);
3. Effects (Extend Base)
import { EntityEffect } from '@cartesianui/common';
@Injectable()
export class ProductEffects extends EntityEffect<Product> {
constructor(httpService: ProductHttpService) {
super(httpService, ProductActions);
}
}
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;
request: RequestState;
get: RequestState;
create: RequestState;
update: RequestState;
delete: RequestState;
}
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 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:
export type IShiftHttpServiceExtension = {
getActiveShifts: (id: string) => Observable<ICartesianResponse>;
endShift: (id: string) => Observable<ICartesianResponse>;
};
@Injectable()
export class ShiftHttpService extends HttpService
implements IHttpService<Shift, IShiftHttpServiceExtension> {
@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 }>()),
};
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);
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 };
}
return newState;
};
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);
}
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 })))
)
)
)
);
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,
});
getActiveShifts(employeeId: string): void {
this.store.dispatch(ShiftActions.fetchActiveShifts({ id: employeeId }));
}
endShift(shiftId: string): void {
this.store.dispatch(ShiftActions.endShift({ id: shiftId }));
}
}
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:
interface EntityStateExtended<T> extends NgRxEntityState<T> {
meta: ResponseMeta | null;
selected: T | null;
request: RequestState | undefined;
get: RequestState | undefined;
create: RequestState | undefined;
update: RequestState | undefined;
delete: RequestState | undefined;
}
When you need additional properties — a separate request tracker, a "current" entity, or a secondary collection — pass an extension interface as the third argument:
export interface ICashDrawerStateExtended {
currentDrawerRequest: RequestState | undefined;
currentDrawer: CashDrawer | null;
}
const stateExtension: ICashDrawerStateExtended = {
currentDrawerRequest: requestDefault,
currentDrawer: null
};
const baseFeature = entityFeature<CashDrawer, ICashDrawerStateExtended>(
'cashDrawers', CashDrawerActions, stateExtension
);
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;
}
};
const stateSelector = (baseFeature as any).selectCashDrawersState;
const currentDrawer = createSelector(stateSelector, (state: any) => state.currentDrawer);
const currentDrawerRequest = createSelector(stateSelector, (state: any) => state.currentDrawerRequest);
export const fromCashDrawer = {
...baseFeature, reducer: customReducer,
currentDrawer, currentDrawerRequest
};
Then consume custom selectors in the sandbox via toSignal():
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);
entities = this.sb.product.entities;
pagination = this.sb.product.pagination;
entities$ = this.sb.product.entities$;
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);
Component Hierarchy
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.
sb.product.entities()
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
cui create library --lib=Catalog --entities=Product,Category
cui create library -l Catalog -e Product,Category -s admin
cui add entity --lib=Catalog --entities=Brand
cui update model --lib=Catalog --entities=Product
cui update form --lib=Catalog --entities=Product
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.
cui extend entity --lib=care --entity=Visit --action=getOpenVisits
cui extend entity -l care -e Visit -a getOpenVisits
cui extend entity -l care -e Visit -a getVisitPrescription
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) => {
};
export const fromProduct = {
...baseFeature,
reducer: customReducer,
fetchRelatedRequest
};
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.