Skip to content

Model Modules

The model layer is where every entity defines its shape, validation rules, errors, and business processes. It is split into three peer packages - base-model, server-model, client-model - that compose into a single namespace at runtime via key-by-key merge in the loader. This document covers all three.

On This Page


Base Model Modules

base-model defines the shared domain model for an entity. It provides canonical data structures, validation, domain rules, and DTO transformations. Pure and IO-free, it is safe to share between server and client.

Base Model - Purpose

  • Define the shared domain model for an entity or domain
  • Provide canonical data structures, validations, domain rules, and DTO transformations
  • Be pure and IO-free so it can be safely shared between server and client when both run JavaScript

Base Model - Design Principles

PrincipleDetail
Domain-focused and deterministicSame input always produces the same output
Pure functions preferredNo hidden state
No database calls, no network callsBelongs in service layer
No secrets, no environment configBelongs in the loader
Explicit inputs and outputsFunction signatures are the contract
Stable contractsData and entity shapes evolve deliberately

Base Model - Naming Convention

ElementConvention
Module directory name[entity-name] (singular, e.g., user, survey)
Location[project]/src/model/[entity-name]/

Base Model - Standard Files

FilePurpose
index.jsPackage entry - returns { Contact: fn, User: fn, Survey: fn, Shared: fn }. Each property is a constructor function. The loader calls Models.Contact(Lib, {}) to execute it and get { data, errors, process, validation, _config }
[entity].config.jsDomain constants and rules (min/max lengths, regex patterns, enums, limits). Domain policy, not environment configuration
[entity].errors.jsDomain error catalog (error codes + default messages + optional HTTP status)
[entity].data.jsConsolidated entity constructors and DTO transformations. All DTOs are derived from the canonical entity shape. Each entity has ONE canonical structure
[entity].process.jsPure business logic - calculations, transformations, collections management. Receives Lib (with Lib.Utils) via the loader pattern
[entity].validation.jsPure validation functions based on [entity].config.js

index.js File Comments and Pattern

CommentFormat
Header// Info: Public export surface for [Entity] base model module
Dependencies note// Dependencies: Contact, User (uses Contact.validation, User.process)
Pattern note// Standard pattern: Loader receives Lib and config override, returns { data, errors, process, validation, _config }

The full index.js template lives in module-structure-js.mdx.

[entity].data.js Function Set

Each entity's data module typically exports:

FunctionPurpose
create(...)Build a complete internal shape with defaults
createUpdate(...)Partial update shape - only provided fields
toPublic(...)Strip server-only fields for API output
toSummary(...)Minimal version for list views
toInternal(...)Map external input to canonical internal shape

Keys not provided (undefined) are simply not added to the resulting object. See DTO Philosophy (JavaScript) for the rationale.

[entity].validation.js Cross-Module Validation

If validation delegates to another model (e.g., User validating an email via Contact), the validation module must use the loader pattern to receive Lib and access Lib.OtherModel.

javascript
// User validation that uses Contact's email validator
// Inside user.validation.js
const result = Lib.Contact.validation.validateEmail(email);

Never directly require() another entity's model from inside a validation file - it bypasses the loader and breaks dependency injection.

DTO Terminology

TermWhere it appears
DTO (Data Transfer Object)Documentation - describes the conceptual pattern of transforming data shapes
DataCode - the actual API uses data for the module name (e.g., user.data.create(), user.data.toPublic())

This is intentional. Docs describe the what (DTO pattern); code implements the how (data module).

Base Model - Boundary Rules

base-model may be used by

  • server-service
  • server-controller
  • Client applications

base-model must NOT

  • Access database or repositories
  • Call external services (SMS, email, payments, ...)
  • Depend on server-only runtime assumptions
  • Contain authorization or policy decisions

Server Model Modules

server-model adds server-only properties and methods to base models - audit trails, internal IDs, admin-only DTOs, policy rules. They are peer packages to the base model, not subclasses. Both packages produce the same shape independently; the loader merges them at runtime.

Peer Package Pattern

RuleDetail
No imports between base and serverServer models do not import or reference the base model internally
Same return shapeBoth packages produce { data, errors, process, validation, _config }
Loader merges key-by-keyObject spread combines the two
Composition, not inheritanceThe merge happens in the loader, not in the modules

Merge Mechanics

The loader is responsible for the merge:

javascript
// Base loads first, assigned to Lib
const SurveyModel = Models.Survey(Lib, {});
Lib.Survey = {
  data: SurveyModel.data,
  errors: SurveyModel.errors,
  process: SurveyModel.process,
  validation: SurveyModel.validation
};

// Server extension loads second (can reference Lib.Survey)
const SurveyModelExtended = ModelsExtended.Survey(Lib, {});

// Loader merges key-by-key (extended adds to or overrides base)
Lib.Survey = {
  data: { ...Lib.Survey.data, ...SurveyModelExtended.data },
  errors: { ...Lib.Survey.errors, ...SurveyModelExtended.errors },
  process: { ...Lib.Survey.process, ...SurveyModelExtended.process },
  validation: { ...Lib.Survey.validation, ...SurveyModelExtended.validation }
};

// Config merged privately, never exposed on Lib
const SurveyConfig = { ...SurveyModel._config, ...SurveyModelExtended._config };

After the merge, callers access Lib.Survey.data.* transparently - both base and server methods are available on the same namespace.

Config Privacy

_config is private. The loader merges base._config + extended._config into a local variable and passes it to service and controller. Never exposed on Lib.Entity.

Structure

server-model follows the same file naming and layout as base-model:

  • index.js, [entity].config.js, [entity].data.js, [entity].errors.js, [entity].process.js, [entity].validation.js
  • Each entity constructor returns { data, errors, process, validation, _config }

What Belongs in Server Model

  • Server-only fields: created_by, organization_id, internal_notes, audit_trail
  • Admin-only DTOs and output shapes
  • Server-side policy logic

What Does NOT Belong Here

  • Base entity shapes (those live in model/)
  • Universal validations (those live in model/)
  • Client-relevant logic (that lives in model-client/)

Server Model - Naming Convention

ElementConvention
Module directory name[entity-name] (singular)
Location[project]/src/model-server/[entity-name]/

Client Model Modules

client-model adds client-relevant properties and methods to base models - client-side metadata, state tracking, lightweight presentation validations. Same peer-package pattern as server-model. Independently loaded; merged by the client-side loader.

What Belongs in Client Model

  • Client-relevant metadata: last_fetched_date, cache_expiry, sync_status
  • Client-side state tracking helpers
  • Lightweight client-only validations (e.g., real-time form checks)
  • Client-specific formatting and presentation helpers

What Does NOT Belong Here

  • Server logic (lives in model-server/)
  • Security-critical validations (must be in model/ or model-server/)
  • Browser-specific or platform-specific APIs (localStorage, window, document)

Merge Pattern

Same as server model - the client-side loader merges base + client extension key-by-key.

Client Model - Naming Convention

ElementConvention
Module directory name[entity-name] (singular)
Location[project]/src/model-client/[entity-name]/

Further Reading

Released under the MIT License.