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 optionallyenums). - 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.tsfor login request/response,refresh-token.tsfor refresh body), not necessarily the genericmodel/create/updateset. 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):
| File | Purpose |
|---|---|
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.tsorauth/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.tsorshared/src/<theme>/enums.tswhen the enum is only used in that scope. (2) App-wide:shared/src/enums.ts(orshared/src/enums/<name>.ts) when used across multiple entities/themes. - Form: Use TypeScript enums (
enum Foo { A = 'a', B = 'b' }) or const objects withas constand a derived type. Enum values (the string or numeric value on the right-hand side, e.g. the'a'inA = 'a') must be in snake_case when they are strings (e.g.Active = 'active',PendingReview = 'pending_review'). For Zod validation (Zod v4): usez.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 usez.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), optionallyshared/src/cat/enums.ts(e.g.CatStatus). - Theme
auth:shared/src/auth/sign-in.ts(e.g.signInSchemawith email/password,SignInDto; optionalSignInResponse),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
schemasfor use in router/controllers. - requests: Backend uses these schemas with
ZodValidationPipeand types from shared; frontend uses the same types and schemas forhandleRequestbodies and form validation (forms). - serialization: If response models use class-transformer, they live in the relevant
model.tsor theme file; backend maps Prisma results withplainToInstance; frontend uses them for response typing andinstanceToPlainfor request bodies where applicable. - responses: Response payload types reference the entity's
modeltype or the theme’s operation types from shared. - validation: Use
z.enum()for enum fields in request/query schemas (Zod v4:z.enum(MyEnum)orz.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.