Skip to main content

Models pattern: shared package layout

All domain models and related types (query params, create/update payloads, enums) live in the main shared package (shared/) so backend and frontends consume the same types and validation surfaces. See pattern sharing for what belongs in shared vs frontends/shared. Organize by one directory per entity or per theme. Two kinds of directories:

  • Entity directory: A single primary domain entity (e.g. cat, user). Uses the standard file set: model, query, create, update (and optionally enums).
  • Theme directory: A cross-cutting or grouped concern (e.g. auth). Contains files named after the specific operations or sub-models (e.g. sign-in.ts for login request/response, refresh-token.ts for refresh body), not necessarily the generic model/create/update set. The theme name is the directory; the "entity" is the operation or concept each file represents.

Entity directories (concrete entities)

Under shared/src/<entity>/ for a concrete entity (e.g. cat, user):

FilePurpose
model (model.ts)Core entity type: domain shape and/or API response type. Used for GET responses, list items, and anywhere the full (or canonical) entity is returned. May use class-transformer decorators if used with serialization; otherwise plain types/interfaces.
query (query.ts)List/filter/search: query params types and (if used) Zod schemas for query validation. E.g. ListCatsQuery with page, limit, search, sortBy.
create (create.ts)Create payload: Zod schema(s) and inferred DTO type for POST body. Used by ZodValidationPipe and requests; frontend uses same types/schemas for forms.
update (update.ts)Update payload: Zod schema(s) and inferred DTO type for PATCH/PUT body. Exclude the resource id—it is passed in the URL (e.g. PATCH /cats/:id). The body contains only updatable fields; typically partial or different from create.
enums (enums.ts)(Optional) Enums used only by this entity. See § Enums below.

Separate create vs update: Keep them separate. Create and update usually differ: create has required fields and no server-generated id; update is often partial (PATCH) and may disallow changing certain fields. The update schema never includes the resource id (it comes from the URL path). Separate files keep validation and types clear and align with distinct controller handlers and form flows.

Theme directories (grouped concerns)

Under shared/src/<theme>/ for a theme (e.g. auth), use descriptive file names for each operation or sub-model, not the generic entity set. The directory groups related request/response shapes and types; there may be no single "entity" name matching the theme.

  • Examples: auth/sign-in.ts (login body: e.g. email + password; optional response type), auth/refresh-token.ts (refresh request body), auth/session.ts or auth/types.ts (shared auth types if needed). Use kebab-case or short names that match the operation: sign-in, refresh-token, verify-email, etc.
  • Optional enums: Add auth/enums.ts (or inline in the same file) when the theme has its own enums (e.g. sign-in method, token type).
  • Theme files export Zod schemas and inferred types; backend and frontend import e.g. shared/auth/sign-in, shared/auth/refresh-token.

Enums

Model enums in shared so backend and frontends share the same values and validation.

  • Where: (1) Per-entity or per-theme: shared/src/<entity>/enums.ts or shared/src/<theme>/enums.ts when the enum is only used in that scope. (2) App-wide: shared/src/enums.ts (or shared/src/enums/<name>.ts) when used across multiple entities/themes.
  • Form: Use TypeScript enums (enum Foo { A = 'a', B = 'b' }) or const objects with as const and a derived type. Enum values (the string or numeric value on the right-hand side, e.g. the 'a' in A = 'a') must be in snake_case when they are strings (e.g. Active = 'active', PendingReview = 'pending_review'). For Zod validation (Zod v4): use z.enum()—it accepts both a TypeScript enum (e.g. z.enum(MyEnum)) and a tuple of string literals (e.g. z.enum(['active', 'pending_review'])). Do not use z.nativeEnum(); it is deprecated in Zod v4. Export both the enum (or const) and any Zod schema that references it so controllers and forms can use the same set of values.

File naming

  • Use singular names for directories: cat, user, auth.
  • Use lowercase for directory and file names: model.ts, query.ts, create.ts, update.ts, enums.ts; for themes, sign-in.ts, refresh-token.ts, etc. (kebab-case for multi-word operation names).

Imports and exports

Consumers use the shared subpath pattern: shared/<entity>/model, shared/<entity>/query, shared/<entity>/create, shared/<entity>/update, shared/<entity>/enums for entities; shared/<theme>/sign-in, shared/<theme>/refresh-token, etc. for themes. The shared package exports must support multi-segment subpaths (e.g. "./*/*": "./dist/*/*") so these directories resolve. Optionally re-export from shared/src/index.ts for a root barrel; per-directory imports from shared/<entity-or-theme>/<file> are the primary convention.

Examples

  • Entity cat: shared/src/cat/model.ts (Cat), shared/src/cat/query.ts (ListCatsQuery, listCatsQuerySchema), shared/src/cat/create.ts (createCatSchema, CreateCatDto), shared/src/cat/update.ts (updateCatSchema, UpdateCatDto), optionally shared/src/cat/enums.ts (e.g. CatStatus).
  • Theme auth: shared/src/auth/sign-in.ts (e.g. signInSchema with email/password, SignInDto; optional SignInResponse), shared/src/auth/refresh-token.ts (refreshTokenSchema, RefreshTokenDto). No single "auth" entity; each file models one operation’s payload (and optionally response).

Integration

  • schemas: Zod schemas for create/update, query, and theme operations live in these files; see schemas for use in router/controllers.
  • requests: Backend uses these schemas with ZodValidationPipe and types from shared; frontend uses the same types and schemas for handleRequest bodies and form validation (forms).
  • serialization: If response models use class-transformer, they live in the relevant model.ts or theme file; backend maps Prisma results with plainToInstance; frontend uses them for response typing and instanceToPlain for request bodies where applicable.
  • responses: Response payload types reference the entity's model type or the theme’s operation types from shared.
  • validation: Use z.enum() for enum fields in request/query schemas (Zod v4: z.enum(MyEnum) or z.enum(['active', 'pending_review']); z.nativeEnum() is deprecated). Enum string values must be snake_case (see § Enums). Backend and frontend then validate the same allowed values.

Implementation

The shared package layout supports both entity directories (standard model, query, create, update, optional enums) and theme directories (operation-named files, e.g. sign-in.ts, refresh-token.ts). Enums live in per-scope enums.ts or in a top-level shared/src/enums.ts. All modules are imported as shared/<dir>/<file>. The shared package.json exports must include a multi-segment pattern (e.g. "./*/*": "./dist/*/*") so these paths resolve. The boilerplate generation skill does not scaffold example entity or theme directories by default; this rule describes the convention for projects and for future skill updates.