Concepts

Contract Kit is organized around a small set of boundaries. The goal is to define the shape of your API once, then reuse that definition across the server, client, validation, docs, and optional application architecture.

Contracts

A contract describes one HTTP endpoint: method, path, path parameters, query parameters, request body, response statuses, response bodies, and metadata.

Contracts are the source of truth for:

Server runtime

The server runtime turns a contract into a route handler. Incoming data is parsed and validated before your handler runs. Handler responses are validated before they are sent when the contract declares response schemas.

export const GET = server.route(getTodo).handle(async ({ ctx, path }) => {
  const todo = await ctx.ports.db.todos.findById(path.id);
  if (!todo) {
    return {
      status: 404,
      body: { code: "TODO_NOT_FOUND", message: "Todo not found" },
    };
  }

  return { status: 200, body: todo };
});

Response ownership

Contract Kit separates responses into three groups:

This keeps the contract strict for business outcomes without forcing every route to declare infrastructure responses such as auth failures, CORS preflights, malformed JSON, or unexpected failures.

Hooks

Hooks are ordered lifecycle functions for infrastructure behavior. Use them for concerns that should not live inside every route handler: auth, CORS, rate limits, logging, tracing, response shaping, and error mapping.

Hooks can short-circuit before the handler, enrich context, observe responses, or map errors.

Ports and providers

Ports are the dependency interface your app code uses through ctx.ports. Providers are production adapters that install or replace ports at server startup.

export const server = await createNextServer({
  ports,
  providers: [loggerPinoProvider, redisProvider],
  createContext: ({ ports }) => ({ ports }),
});

Tests can pass mock ports directly, while production can install Redis, Pino, Better Auth, Turso, Inngest, mail providers, and other adapters.

Client calls

The client has two request styles:

Use call() with React Query and most UI data-fetching flows. Use safeCall() when explicit branching reads better than exceptions.

Application and domain

The application package defines reusable use cases with input/output validation. Use it when the same business operation should run from HTTP routes, jobs, scripts, tests, or event handlers.

The domain package provides optional helpers for value objects, entities, and domain event declarations. Use it when you want more structure around core business concepts.