Maintain contracts
Shared TypeScript types and Zod schemas live in core/contracts (@core/contracts). They define the contract between the BFF and the presentation app so both sides validate and type the same payloads.
Shared configuration literals (locales, flags, data-source keys) belong in Config & constants, not here.
Why Zod
- Runtime — Schemas validate data at the boundary (BFF responses, request bodies). Bad shapes are caught instead of propagating.
- Types — Use
z.infer<typeof Schema>so TypeScript tracks the schema as the single source of truth. - Shared — BFF and storefront import the same definitions from
@core/contracts.
Layout
src/core/— Shared bases (entities, prices, badges, links, etc.).src/<domain>/— Domain groupings (examples in the repo may includeproduct,cart,content,navigation—treat names as illustrative).
Organise by domain, not by “response file” vs “request file”. Related request and response schemas live in the same domain folder.
Naming
Schemas
- Suffix Zod exports with *Schema (e.g.
ProductDetailsResponseSchema,BaseEntityResponseSchema). - Build with
z.object(),z.discriminatedUnion(),.extend(), and composition as needed.
Types
- Infer public types with
z.infer<typeof SomeSchema>. - Prefer *Response / *Request names for payloads that cross the API boundary (see table below). For pure domain shapes that are not tied to a single direction, naming follows what exists in the package.
Types vs responses / requests
| Kind | Use case | Zod schema suffix | TypeScript type |
|---|---|---|---|
| Types | Shared domain / base shapes | (no fixed suffix) | As defined in the package |
| Responses | Incoming API / external data | *ResponseSchema | *Response (inferred) |
| Requests | Outgoing API / client payloads | *RequestSchema | *Request (inferred) |
Patterns
- Extend base schemas with
.extend()when a detail view adds fields to a shared entity. - Discriminated unions for variants (e.g. options with a type discriminator).
- Composition over deep inheritance where it keeps schemas easier to reuse.
Consume
Import through @core/contracts with subpaths that mirror the compiled layout under dist/ (e.g. @core/contracts/product/product-details). In the accelerator, package.json exports uses a ./* → ./dist/*.js wildcard, so a new source file does not require editing exports—run the package build so dist/ includes the new module. If your fork changes exports, follow that file as the source of truth.
Add or change a contract
- Place the file under the right domain (e.g.
src/core/orsrc/<domain>/). - Define the Zod schema with a *Schema name.
- Export inferred types (*Response / *Request or existing conventions in that folder).
- Build — Run
npm run buildincore/contracts(or the monorepo build that covers it) so the new file is emitted underdist/with the same relative path as undersrc/. With the repo’s wildcardexports, no per-file export or barrel update is needed unless you have changedpackage.jsonaway from that pattern.
Typecheck
From the repository root, run npm run check-types to catch contract drift, bad imports, and cross-workspace type errors.
Related
- Config & constants — Shared literals and enums, not API DTOs
- BFF communication — How the storefront talks to the BFF
- Data sources — Adapters and factory
- Feature — Consuming contract types in feature code
- General workflow rules — Pre-commit and quality gates