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:
- example-next — basic todo app with React Query
- example-contacts — contact management with domain modeling, event bus, and React Hook Form
- example-blog — blog app with the full application layer
Each example is a standalone Next.js app you can run locally. See the GitHub repo for details.