Examples

A complete todo API built with Contract Kit and Next.js.

Project structure

app/
├── api/
│   ├── todos/
│   │   └── route.ts          # list + create handlers
│   └── todos/[id]/
│       └── route.ts          # get + update + delete handlers
├── contracts/
│   └── todo.ts               # all todo contracts
├── server/
│   ├── index.ts              # server setup
│   └── ports.ts              # ports definition
└── lib/
    └── api-client.ts         # typed client

Contracts

// app/contracts/todo.ts
import { createContractGroup } from "contract-kit";
import { z } from "zod";

const todos = createContractGroup().namespace("todos");

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

export const listTodos = todos
  .get("/api/todos")
  .query(z.object({
    completed: z.boolean().optional(),
  }))
  .response(200, z.object({
    todos: z.array(TodoSchema),
  }));

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

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

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)
  .errors({ 404: z.object({ message: z.string() }) });

export const deleteTodo = todos
  .delete("/api/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(204)
  .errors({ 404: z.object({ message: z.string() }) });

Server

// app/server/ports.ts
import { definePorts } from "contract-kit";

type Todo = { id: string; title: string; completed: boolean };

const store = new Map<string, Todo>();

export const ports = definePorts({
  db: {
    todos: {
      findAll: async (filter?: { completed?: boolean }) => {
        const all = Array.from(store.values());
        if (filter?.completed !== undefined) {
          return all.filter((t) => t.completed === filter.completed);
        }
        return all;
      },
      findById: async (id: string) => store.get(id) ?? null,
      create: async (data: { title: string; completed?: boolean }) => {
        const todo = { id: crypto.randomUUID(), title: data.title, completed: data.completed ?? false };
        store.set(todo.id, todo);
        return todo;
      },
      update: async (id: string, data: { title?: string; completed?: boolean }) => {
        const todo = store.get(id);
        if (!todo) return null;
        const updated = { ...todo, ...data };
        store.set(id, updated);
        return updated;
      },
      delete: async (id: string) => store.delete(id),
    },
  },
});
// app/server/index.ts
import { createNextServer } from "@contract-kit/next";
import { ports } from "./ports";

export const server = await createNextServer({
  ports,
  createContext: ({ ports }) => ({
    requestId: crypto.randomUUID(),
    ports,
  }),
  onUnhandledError: () => ({
    status: 500,
    body: { message: "Internal server error" },
  }),
});

Route handlers

// app/api/todos/route.ts
import { server } from "@/app/server";
import { listTodos, createTodo } from "@/app/contracts/todo";

export const GET = server.route(listTodos).handle(async ({ ctx, query }) => {
  const todos = await ctx.ports.db.todos.findAll(query);
  return { status: 200, body: { todos } };
});

export const POST = server.route(createTodo).handle(async ({ ctx, body }) => {
  const todo = await ctx.ports.db.todos.create(body);
  return { status: 201, body: todo };
});
// app/api/todos/[id]/route.ts
import { server } from "@/app/server";
import { getTodo, updateTodo, deleteTodo } from "@/app/contracts/todo";

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: { message: "Todo not found" } };
  return { status: 200, body: todo };
});

export const PATCH = server.route(updateTodo).handle(async ({ ctx, path, body }) => {
  const todo = await ctx.ports.db.todos.update(path.id, body);
  if (!todo) return { status: 404, body: { message: "Todo not found" } };
  return { status: 200, body: todo };
});

export const DELETE = server.route(deleteTodo).handle(async ({ ctx, path }) => {
  const deleted = await ctx.ports.db.todos.delete(path.id);
  if (!deleted) return { status: 404, body: { message: "Todo not found" } };
  return { status: 204 };
});

Client

// app/lib/api-client.ts
import { createClient } from "contract-kit";
import { listTodos, getTodo, createTodo, updateTodo, deleteTodo } from "@/app/contracts/todo";

const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
});

export const listTodosEndpoint = client.endpoint(listTodos);
export const getTodoEndpoint = client.endpoint(getTodo);
export const createTodoEndpoint = client.endpoint(createTodo);
export const updateTodoEndpoint = client.endpoint(updateTodo);
export const deleteTodoEndpoint = client.endpoint(deleteTodo);

Usage

// List all todos
const { todos } = await listTodosEndpoint.call({});

// Create a todo
const todo = await createTodoEndpoint.call({
  body: { title: "Buy groceries" },
});

// Update a todo
await updateTodoEndpoint.call({
  path: { id: todo.id },
  body: { completed: true },
});

// Delete a todo
await deleteTodoEndpoint.call({
  path: { id: todo.id },
});

Example apps

The Contract Kit repo includes three complete example applications:

Each example is a standalone Next.js app you can run locally. See the GitHub repo for details.