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.
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
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
const customer = Customer.fromJson(apiResponse);
const payload = customer.toJson();
const formGroup = customer.toFormGroup();
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.
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
Actions (One-liner)
const actions = createEntityActions<Account>('Account');
export const AccountActions = { ...actions };
Effects (Extend Base)
@Injectable()
export class AccountEffects extends BaseEntityEffects<Account> {
constructor(actions$: Actions, http: AccountHttpService) {
super(http, AccountActions);
}
}
Reducer (One-liner)
export const fromAccount = createEntityFeature<Account>('accounts', AccountActions);
Entity State Structure
interface EntityStateExtended<T> extends EntityState<T> {
meta: ResponseMeta | null;
request: RequestState;
create: RequestState;
update: RequestState;
delete: RequestState;
selected: T | null;
}
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:
const baseActions = createEntityActions<Account>('Account');
export const AccountActions = {
...baseActions,
approveAccount: createAction(
'[Account] Approve Account',
props<{ id: string }>()
),
approveAccountSuccess: createAction(
'[Account] Approve Account Success',
props<{ account: Account }>()
),
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);
}
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 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:
const baseFeature = createEntityFeature<Account>('accounts', AccountActions);
export const accountReducer = createReducer(
baseFeature.initialState,
...baseFeature.reducerCases,
on(AccountActions.approveAccountSuccess, (state, { account }) =>
baseFeature.adapter.updateOne(
{ id: account.id, changes: { status: 'approved' } },
state
)
),
on(AccountActions.archiveMultiple, (state, { ids }) => ({
...state,
update: { started: true, completed: false, failed: false }
}))
);
export const fromAccount = {
...baseFeature,
reducer: accountReducer
};
Adding Custom Selectors
Create derived selectors for computed state:
const { selectAll, selectEntities, selectSelected } = fromAccount.selectors;
export const selectActiveAccounts = createSelector(
selectAll,
(accounts) => accounts.filter(a => a.status === 'active')
);
export const selectTotalBalance = createSelector(
selectAll,
(accounts) => accounts.reduce((sum, a) => sum + a.balance, 0)
);
export const selectAccountsByType = (type: string) => createSelector(
selectAll,
(accounts) => accounts.filter(a => a.type === type)
);
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:
interface AccountState extends EntityStateExtended<Account> {
filterStatus: string | null;
pendingApprovals: string[];
lastSyncedAt: Date | null;
}
const initialState: AccountState = {
...baseFeature.initialState,
filterStatus: null,
pendingApprovals: [],
lastSyncedAt: null
};
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 {
account = new EntitySandbox<Account>(this.store, AccountActions, fromAccount);
constructor(private store: Store) {
super();
}
}
Usage in Component
export class AccountListComponent {
sb = inject(BookeeperSandbox);
accounts$ = this.sb.account.entities$;
accounts = this.sb.account.entities;
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;
}
@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);
Component Hierarchy
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);
private readonly busyEffect = effect(() => {
this.handleBusyState(this.sb.entity.getState());
});
entities = this.sb.entity.entities;
pagination = this.sb.entity.pagination;
}
Key Architectural Patterns
Facade Pattern (Sandbox)
Sandbox services abstract NgRx store complexity, providing a clean API for components.
this.sb.fetchUsers(criteria);
this.sb.users$.subscribe(users => ...);
Generic Entity Operations
EntitySandbox, entityActions(), and EntityEffect provide reusable CRUD abstractions.
const actions = entityActions<User>('User');
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).
sb.entities$: Observable<T[]>
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
ng generate @cartesianui/ng-cli:library bookeeper
ng generate @cartesianui/ng-cli:entity account --library=bookeeper
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.