contract-first-api is a small TypeScript toolkit for defining an API contract once and reusing it across your stack.
The typical workflow is:
- Define a contract tree with
@contract-first-api/core. - Mount it on Express with
@contract-first-api/express. - Create a typed runtime client with
@contract-first-api/api-client. - Optionally wrap that client with
@contract-first-api/react-query.
The same contract tree can also carry metadata, so runtime middleware, request context creation, and client-side transforms can all read from one shared definition.
@contract-first-api/core: define contracts, metadata, and shared request/response types@contract-first-api/express: register contracts as Express routes with typed services and middleware@contract-first-api/api-client: create a typed runtime client from the contract tree@contract-first-api/react-query: turn the typed client into React Query hooks and cache helpers
pnpm add @contract-first-api/coreAdd the other packages you need:
pnpm add @contract-first-api/express @contract-first-api/api-client @contract-first-api/react-queryimport { initContracts } from "@contract-first-api/core";
import z from "zod";
type ContractMeta = {
requiresAuth?: boolean;
auditLabel?: string;
};
const { defineContract } = initContracts<ContractMeta>();
export const contracts = defineContract({
health: {
get: {
method: "GET",
path: "/health",
meta: {
auditLabel: "health.get",
},
response: z.object({
status: z.literal("ok"),
}),
},
},
todos: {
list: {
method: "GET",
path: "/todos",
response: z.object({
items: z.array(
z.object({
id: z.string(),
title: z.string(),
}),
),
}),
},
create: {
method: "POST",
path: "/todos",
meta: {
requiresAuth: true,
auditLabel: "todos.create",
},
request: {
body: z.object({
title: z.string().min(1),
}),
},
response: z.object({
id: z.string(),
title: z.string(),
}),
},
},
});From there, the backend, client, and optional React Query layer all reuse the same contract tree and stay aligned on request and response shapes.
MIT