Skip to main content

What are Contracts?

A contract is the single source of truth for an API endpoint. It describes everything about an HTTP endpoint in a type-safe way:
  • HTTP method and path (with path parameters)
  • Path parameters, query parameters, and request body schemas
  • Response schemas (per status code)
  • Error schemas (per status code)
  • Metadata for auth, rate limiting, idempotency, etc.

Why Contracts?

Contracts provide several key benefits:
  1. Type Safety: Full TypeScript inference from contract to server to client
  2. Documentation: Self-documenting code that can generate OpenAPI specs
  3. Validation: Automatic request/response validation at runtime
  4. Consistency: Single definition ensures server and client stay in sync
  5. No Codegen: Type inference happens at compile time, no build steps needed

Creating a Contract Group

Contract groups let you share configuration across related endpoints:
import { createContractGroup } from "contract-kit";
import { z } from "zod";

const todos = createContractGroup()
  .namespace("todos") // Optional: prefix for OpenAPI tags
  .meta({ auth: "required" }) // Optional: shared metadata
  .errors({ 401: z.object({ message: z.string() }) }); // Optional: shared error schemas

Defining Contracts

GET Requests

import { z } from "zod";

export const getTodo = todos
  .get("/api/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
  }));

POST Requests

export const createTodo = todos
  .post("/api/todos")
  .body(z.object({
    title: z.string().min(1),
    completed: z.boolean().optional(),
  }))
  .response(201, z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
  }));

Query Parameters

export const listTodos = todos
  .get("/api/todos")
  .query(z.object({
    completed: z.boolean().optional(),
    limit: z.number().int().min(1).max(100).optional(),
    offset: z.number().int().min(0).optional(),
  }))
  .response(200, z.object({
    todos: z.array(TodoSchema),
    total: z.number(),
  }));

Path Parameters

export const updateTodo = todos
  .patch("/api/todos/:id")
  .path(z.object({ id: z.string() }))
  .body(z.object({
    title: z.string().optional(),
    completed: z.boolean().optional(),
  }))
  .response(200, TodoSchema);

Multiple Response Status Codes

export const getTodo = todos
  .get("/api/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, TodoSchema)
  .response(404, z.object({ message: z.string() }));

Error Schemas

Define expected error responses:
export const createTodo = todos
  .post("/api/todos")
  .body(CreateTodoSchema)
  .response(201, TodoSchema)
  .errors({
    400: z.object({
      message: z.string(),
      errors: z.array(z.string()),
    }),
    401: z.object({ message: z.string() }),
  });

Contract Metadata

Contracts can include metadata for various purposes:

Authentication

export const getProtectedData = todos
  .get("/api/protected")
  .meta({ auth: "required" }) // Requires authentication
  .response(200, DataSchema);

Rate Limiting

export const createTodo = todos
  .post("/api/todos")
  .meta({ rateLimit: { max: 10, windowSec: 60 } })
  .body(CreateTodoSchema)
  .response(201, TodoSchema);

Idempotency

export const createPayment = payments
  .post("/api/payments")
  .meta({ idempotency: { enabled: true } }) // Requires idempotency key
  .body(PaymentSchema)
  .response(201, PaymentResultSchema);

Schema Libraries

Contract Kit works with any Standard Schema library:
import { z } from "zod";

const TodoSchema = z.object({
  id: z.string(),
  title: z.string(),
  completed: z.boolean(),
});

Best Practices

Each contract should represent a single endpoint. Don’t try to handle multiple operations in one contract.
Include validation rules in your schemas (min/max length, regex patterns, etc.) to catch errors early.
Use your schema library’s description features to add documentation that will appear in OpenAPI specs.
Include version numbers in paths (e.g., /api/v1/todos) for long-term API stability.

Next Steps