This file provides essential context for working with the express-zod-api project.
The express-zod-api is a TypeScript framework for building Express.js APIs with Zod schema validation. It provides type-safe request/response handling, automatic OpenAPI documentation, and a clean API builder pattern.
- Package manager: pnpm
- Current version and Node version: See
express-zod-api/package.json
This is a pnpm monorepo. The directory name becomes the workspace name.
├── express-zod-api/ # Main package
│ ├── src/ # TypeScript source
│ └── tests/ # Unit tests (Vitest)
├── migration/ # Workspace: @express-zod-api/migration
├── example/ # Example API server
├── zod-plugin/ # Workspace: @express-zod-api/zod-plugin
├── cjs-test/ # CommonJS compatibility tests
├── esm-test/ # ESM compatibility tests
├── compat-test/ # Compatibility tests
└── issue952-test/ # Issue reproduction tests
| Directory | Package Name | Description |
|---|---|---|
express-zod-api/ |
express-zod-api |
Framework itself (main) |
migration/ |
@express-zod-api/migration |
ESLint migration rules |
zod-plugin/ |
@express-zod-api/zod-plugin |
Zod plugin for schema enhancements |
example/ |
— | Usage example |
The rest are test workspaces.
Quick reference on how the key modules connect:
flowchart LR
A[config-type.ts] -->|creates| B[createConfig]
C[endpoints-factory.ts] -->|builds| D[Endpoint]
D --> E[endpoint.ts]
E --> F[routing.ts]
F --> G[server.ts]
See src/index.ts for the complete public API exports.
express-zod-api/src/index.ts— Public API exportsexpress-zod-api/src/config-type.ts— Configuration types forcreateConfig()express-zod-api/src/routing.ts— Core routing logicexpress-zod-api/src/testing.ts— Test utilities:testEndpointandtestMiddlewareare public, others internalexpress-zod-api/tests/express-mock.ts— Express mocking for testsmigration/index.ts— Migration ESLint rulesCHANGELOG.md— Version history and breaking changes
- Source files:
src/*.ts - Test files:
tests/*.spec.ts(Vitest) - Test organization: In
express-zod-apiworkspace,tests/mirrorssrc/. Each source file has a corresponding test file with the same name.
Everything exported via express-zod-api/src/index.ts is part of the public API.
All properties of publicly available entities (exposed via index.ts) must have JSDoc documentation using directives:
@desc: Short description of what the property does, ends with a period@default: Default value (required for optional properties)@example: Example value (required for literal types, one per variant)
Each directive should aim to fit on one line:
interface SampleInterface {
/** @desc Enables certain feature. */
sampleRequiredProperty: boolean | SampleOptions;
/** @desc Controls another feature. */
/** @default true */
/** @example true — leads to one thing */
/** @example false — leads to another thing */
sampleOptionalProperty?: boolean;
}- Zod: Use named import
import { z } from "zod" - Ramda: Use namespace import
import * as R from "ramda" - Node.js built-ins: Use
node:prefix - Type-only imports: Use
import typefor types and interfaces - Relative imports: Must be extensionless
- Combine import from the same module into a single line
import { z } from "zod";
import * as R from "ramda";
import { dirname } from "node:path";
import type { SomeType } from "./module-a";
import { someValue } from "./module-b";
import { anotherValue, type AnotherType } from "./module-c";Object-based types should be declared as interfaces, not types:
// Good
interface User {
name: string;
age: number;
}
// Avoid
type User = { name: string; age: number };- Use
test.each(): Always prefer parameterized tests to reduce repetition - Placeholders: Use
%sfor the current value and%#for the index
test.each([true, false, undefined])(
"Should handle hintAllowedMethods=%s",
(hintAllowedMethods) => {},
);- Mock patterns: Use utilities from
src/testing.ts(testEndpoint,testMiddleware, etc.) and Express mocks fromtests/express-mock.ts:
const { loggerMock, requestMock, responseMock } = await testEndpoint({
endpoint,
requestProps: { method: "GET" },
responseOptions: { locals: {} },
});- Test runner: Vitest
- Location:
tests/directories within each package
Public API config uses CommonConfig interface with JSDoc:
interface CommonConfig {
/** @desc Description of what this config does */
propertyName?: type; // default: value
}const factory = new EndpointsFactory(defaultResultHandler);
const endpoint = factory.build({
method: "get",
path: "/users",
input: z.object({ ... }),
output: z.object({ ... }),
handler: async ({ input }) => ({ users: [] }),
});- Build tool:
tsdown - Commands:
pnpm build(root builds all workspaces)
- Maximum line length: 120 characters (including Markdown files)
- Use line breaks and code folding to stay within limit
When moving statements, preserve any inline comments from the source location:
// Original:
if (entry.description) flat.description ??= entry.description; // can be empty
// After extracting (comment preserved in place):
const copyDescription = (entry, flat) => {
if (entry.description) flat.description ??= entry.description; // can be empty
};Markdown files should avoid --- horizontal rule separators.
For generating TypeScript code, prefer using helpers from TypescriptAPI class over direct factory methods. Native
factory methods have verbose APIs with many redundant arguments.
Files that generate TypeScript code:
express-zod-api/src/typescript-api.ts(defines the helpers)express-zod-api/src/integration.tsexpress-zod-api/src/integration-base.tsexpress-zod-api/src/zts.ts
Examples: Use these helpers instead of verbose factory calls:
// Use TypescriptAPI methods:
api.makeId("User");
api.makeParam("name", { type: "string" });
api.makeConst("count", api.literally(0));
api.makeInterface("User", [api.makeInterfaceProp("name", "string")]);
// Avoid native factory calls:
ts.factory.createIdentifier("User");
ts.factory.createParameterDeclaration();
ts.factory.createVariableStatement();For code involved in API execution (routing, middlewares, schema definition, validation), performance is the highest
priority. This does not apply to generators (Integration, Documentation).
When editing that code or choosing between multiple implementations in performance-critical scope, always A/B test it:
- Create a benchmark in
express-zod-api/bench(similar toexperiment.bench.ts) - Compare current vs featured solution (for refactoring) or two variants (for new code)
- Run benchmark via
pnpm bench
describe("Experiment for feature", () => {
bench("current", () => {}); // implementation A
bench("featured", () => {}); // implementation B
});Any breaking change to the public API requires an update to the migration script, its tests, and CHANGELOG.
The migration is implemented as an ESLint rule in migration/index.ts:
Queriesinterface — defines AST node types to search for using esquery selectorsqueriesobject — maps query names to esquery selectorslisten()function — connects queries to their handler functions increate()create()function — implements the actual transformation
Tests are in migration/index.spec.ts using RuleTester:
valid— code that should NOT trigger the ruleinvalid— code that SHOULD be transformed, with expectedoutputand error assertions
The CHANGELOG.md follows these rules:
- Structure: Nested in reverse order (the most recent version at top)
- Level 2: Major version title, e.g.,
## Version N - Level 3: Specific version, e.g.,
### vN.N.N(semantic) - Changes: Single list, may have nested items for clarifications
- Punctuation:
- Item with nested items ends with
: - Last item in a list ends with
. - Other items end with
;
- Item with nested items ends with
- Code samples: Prefer a single sample below the list
- Major releases (vN.0.0): Use
diffcode block with+/-prefixes and spacing offset
pnpm build # Build all workspacesNote: Build everything before testing everything, because example workspace depends on express-zod-api.
pnpm build # Build all workspaces first
pnpm test # Run all tests
pnpm -F express-zod-api test # Run main package tests only
pnpm -F migration test # Run migration tests
pnpm -F express-zod-api test -- --run routing # Run specific test filepnpm lint # Check lint and formatting
pnpm mdfix # Fix formatting of markdown files