Server
The server runtime handles HTTP requests with full type safety, automatic validation, and middleware support.
Creating a server
import { createNextServer } from "@contract-kit/next";
import { definePorts } from "contract-kit";
const ports = definePorts({
db: {
todos: {
findById: async (id: string) => ({ id, title: "Example", completed: false }),
},
},
});
export const server = await createNextServer({
ports,
createContext: ({ ports, req }) => ({
requestId: crypto.randomUUID(),
userId: req.headers.get("x-user-id"),
ports,
}),
onUnhandledError: (error) => ({
status: 500,
body: { message: "Internal server error" },
}),
});
Ports
Ports define your application's external dependencies — databases, caches, mailers, etc. Using definePorts gives you full type inference throughout your handlers.
import { definePorts } from "contract-kit";
export const ports = definePorts({
db: { /* your database adapter */ },
cache: { /* your cache adapter */ },
mail: { /* your mail adapter */ },
});
See Providers for ready-made implementations.
Context
The createContext function runs on every request. It receives ports and the raw request, and returns a context object available to all handlers.
createContext: ({ ports, req }) => ({
requestId: crypto.randomUUID(),
userId: req.headers.get("x-user-id") || null,
ports,
}),
Route handlers
Use server.route(contract).handle() to implement an endpoint. The handler receives validated, typed parameters.
import { server } from "@/app/server";
import { getTodo } 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 };
});
The handler object gives you:
req— the raw HTTP requestctx— your context object (with ports, user info, etc.)path— validated path parametersquery— validated query parametersbody— validated request bodymeta— contract metadata (if defined)
All values are fully typed based on the contract definition.
export const POST = server.route(createTodo).handle(async ({ ctx, body }) => {
const todo = await ctx.ports.db.todos.create({
title: body.title,
completed: body.completed ?? false,
});
return { status: 201, body: todo };
});
Middleware
Global middleware
Add middleware that runs on every request.
export const server = await createNextServer({
ports,
middleware: [
async ({ req, ctx, meta, next }) => {
const start = Date.now();
const response = await next(ctx);
console.log(`${req.method} ${req.url} - ${Date.now() - start}ms`);
return response;
},
],
createContext: ({ ports }) => ({ ports }),
});
Metadata-aware middleware
Use contract metadata to conditionally apply behavior.
const authMiddleware = async ({ req, meta, ctx, next }) => {
if (meta?.auth !== "required") {
return next(ctx);
}
const token = req.headers.get("authorization");
if (!token) {
return { status: 401, body: { message: "Unauthorized" } };
}
return next(ctx);
};
This works because contracts can declare metadata like .meta({ auth: "required" }). The middleware checks it and decides whether to enforce the behavior.
Error handling
Automatic validation
Contract Kit automatically validates incoming requests against your contract schemas. If validation fails, it returns a 400 response with error details. Your handler only runs if the request is valid.
Custom errors
Return error responses that match your contract's error 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: { message: "Todo not found" } };
}
return { status: 200, body: todo };
});
Global error handler
The onUnhandledError callback catches any unhandled exceptions.
onUnhandledError: (error, { req, ctx }) => {
console.error("Unhandled error:", error);
return {
status: 500,
body: { message: "Internal server error" },
};
},
Next.js integration
The @contract-kit/next adapter works with the Next.js App Router. Route files export the handler directly.
// 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);
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);
return { status: 200, body: todo };
});
export const DELETE = server.route(deleteTodo).handle(async ({ ctx, path }) => {
await ctx.ports.db.todos.delete(path.id);
return { status: 204 };
});