SHOPin Logo
Skip to main documentation content

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 include product, 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

KindUse caseZod schema suffixTypeScript type
TypesShared domain / base shapes(no fixed suffix)As defined in the package
ResponsesIncoming API / external data*ResponseSchema*Response (inferred)
RequestsOutgoing 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

  1. Place the file under the right domain (e.g. src/core/ or src/<domain>/).
  2. Define the Zod schema with a *Schema name.
  3. Export inferred types (*Response / *Request or existing conventions in that folder).
  4. Build — Run npm run build in core/contracts (or the monorepo build that covers it) so the new file is emitted under dist/ with the same relative path as under src/. With the repo’s wildcard exports, no per-file export or barrel update is needed unless you have changed package.json away 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

Back to Building blocks · Back to How to work with SHOPin