TypeScript DDD DTO
MANDATORY — READ ENTIRE FILE: Before any implementation step, read
references/dto-pattern.md completely.
Do NOT load other DDD skills (entity, use-case, controller) unless explicitly requested.
Where DTOs live
DTOs are not co-located with the api. They live in the per-BC contracts package, which is the single source of truth for the wire shape and is consumed by both apps/api and apps/web.
libs/contracts/<bc>/src/
interfaces/ ← pure TS types + string-backed `XxxEnum` for closed sets
schemas/ ← Zod 4 schemas (derive types via z.infer)
dtos/ ← DTO types paired with their Zod schema
index.ts ← re-exports interfaces + schemas + dtos
Imported as @acme/<bc>-contracts (e.g. @acme/celebrations-contracts). Neither api nor web redefines wire types locally. The api maps contract DTOs to entities via apps/api/src/<bc>/presentation/mappers/.
Before You Start
Non-obvious decisions specific to this monorepo:
- Does this wire shape already exist in
libs/contracts/<bc>/? If yes, edit it; never duplicate it insideapps/apiorapps/web. - Write-side input (
CreateXxxDTO/UpdateXxxDTO/XxxFiltersDTO) or read-side projection (XxxResponseDTO/XxxListItemDTO)? They are separate DTOs; do not reuse one as the other. - Does any field belong to a closed set? It MUST come from an
XxxEnumininterfaces/— see the fallback rule inreferences/dto-pattern.mdfor where the enum lives (BC vslibs/shared). - Is the shape new, an added field, or a new variant in a discriminated union? Use the decision tree at the top of
references/dto-pattern.mdto pick the right file(s).
DTO Types
| Type | Suffix / name | Purpose |
|---|---|---|
| Input | CreateXxxDTO, UpdateXxxDTO, XxxFiltersDTO |
Command input / query filters |
| Output | XxxResponseDTO |
Controller response / use case payload |
| Query | XxxDTO, XxxDetailsDTO, XxxListItem |
CQRS read projection |
Mandatory: enums for closed-set values
Every closed set is a string-backed TS enum in interfaces/. The tuple, type, and predicate derive from it:
export enum SectionKindEnum {
HERO = "hero",
MESSAGE = "message",
// ...
}
export const SECTION_KINDS = Object.values(SectionKindEnum);
export type SectionKind = (typeof SectionKindEnum)[keyof typeof SectionKindEnum];
export function isSectionKind(v: unknown): v is SectionKind {
return typeof v === "string" && SECTION_KINDS.includes(v as SectionKind);
}
Zod usage:
z.literal(SectionKindEnum.HERO)— branch of az.discriminatedUnion(...).z.nativeEnum(CelebrationStatusEnum)— full-set field validator.z.enum(StringTuple)— only when no enum exists (e.g.PALETTE_KEYSfrom@acme/shared).
Never inline string literals like z.literal("hero") or z.enum(["draft", "published"]) when an enum exists.
Core Rules
- Zod is the single validation source. Declare the schema first; export the type via
z.infer. - Query DTOs never extend an entity class — they are independent shapes that describe the wire payload.
- DTOs carry no domain logic and no Firestore / NestJS / Next imports.
- Enrichment for the front (signed URLs, formatted strings, expanded relations) happens at the api boundary (
presentation/mappers/or a use-case projection), not inside the DTO file. - Closed-set fields reference an
XxxEnum. Discriminated unions useEnumName.MEMBERin both the TS branch (type: SectionKindEnum.HERO) and the Zod branch (z.literal(SectionKindEnum.HERO)). - A change in
libs/contracts/<bc>is a wire-contract change: update api validation/mappers and web consumers in the same PR.
NEVER
- NEVER redefine a wire type inside
apps/apiorapps/web— import it from@acme/<bc>-contracts. - NEVER reuse a
CreateXxxDTOas aXxxResponseDTO— command fields leak into responses. - NEVER import
firebase-admin,@nestjs/*,next, orreactfrom a contracts package. - NEVER hand-write an interface that mirrors a Zod schema — derive with
z.infer. - NEVER use string literals or
as consttuples for a closed set when anXxxEnumexists or should exist. - NEVER put transformation logic in the DTO file — map at the api boundary.
- NEVER embed pagination totals inside the
dataarray — keepdataandmetaseparate.
References
See references/dto-pattern.md for: contracts-package layout, the enum → tuple → type → predicate pattern, schema/DTO snippets, naming conventions, and the verification checklist (pnpm --filter @acme/<bc>-contracts test → build → pnpm --filter api typecheck → pnpm --filter web typecheck).