Skip to main content

Overview

The Contract Kit server runtime provides a framework-agnostic way to handle HTTP requests with full type safety, validation, and middleware support.

Server Architecture

The server runtime consists of:
  1. Adapters: Framework-specific implementations (Next.js, Express, etc.)
  2. Request Pipeline: Parsing, validation, middleware execution
  3. Context: Request-scoped data and dependencies
  4. Error Handling: Automatic error mapping and responses

Creating a Server

Basic Setup

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" },
  }),
});

Implementing Routes

Handle Function

The handle function is where you implement your business logic:
import { server } from "@/lib/server";
import { getTodo } from "@/contracts/todo";

export const GET = server.route(getTodo).handle(async ({ ctx, path, query, body }) => {
  // ctx: Your context object
  // path: Validated path parameters
  // query: Validated query parameters
  // body: Validated request body

  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 };
});

Accessing Request Data

All request data is automatically parsed and validated:
export const POST = server.route(createTodo).handle(async ({ ctx, body }) => {
  // body is fully typed based on your contract
  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 }) => {
      console.log(`${req.method} ${req.url}`);
      const start = Date.now();
      const response = await next(ctx);
      console.log(`Completed in ${Date.now() - start}ms`);
      return response;
    },
  ],
  createContext: ({ ports }) => ({ ports }),
});

Metadata-Aware Middleware

Use contract metadata to opt into middleware behavior:
const authMiddleware = async ({ req, meta, next }) => {
  if (meta?.auth !== "required") {
    return next();
  }

  const token = req.headers.get("authorization");
  if (!token) {
    return { status: 401, body: { message: "Unauthorized" } };
  }

  return next();
};

Context

Creating Context

Context is created per-request and provides access to dependencies:
export const server = await createNextServer({
  ports,
  createContext: ({ ports, req }) => ({
    // Add request-scoped data
    requestId: crypto.randomUUID(),
    timestamp: Date.now(),
    
    // Extract user information
    userId: req.headers.get("x-user-id") || null,
    
    // Include ports for dependency injection
    ports,
  }),
});

Using Context

Access context in your route handlers:
export const GET = server.route(getTodo).handle(async ({ ctx, path }) => {
  console.log(`Request ${ctx.requestId} by user ${ctx.userId}`);
  
  const todo = await ctx.ports.db.todos.findById(path.id);
  return { status: 200, body: todo };
});

Error Handling

Automatic Validation Errors

Contract Kit automatically validates requests and returns appropriate errors:
// If validation fails, returns 400 with error details
export const POST = server.route(createTodo).handle(async ({ body }) => {
  // body is guaranteed to be valid here
  // ...
});

Custom Error Handling

Handle specific errors in your route:
export const GET = server.route(getTodo).handle(async ({ ctx, path }) => {
  try {
    const todo = await ctx.ports.db.todos.findById(path.id);
    return { status: 200, body: todo };
  } catch (error) {
    if (error instanceof NotFoundError) {
      return { status: 404, body: { message: "Todo not found" } };
    }
    throw error; // Let global error handler deal with it
  }
});

Global Error Handler

Define a catch-all error handler:
export const server = await createNextServer({
  ports,
  createContext: ({ ports }) => ({ ports }),
  onUnhandledError: (error, { req }) => {
    console.error("Unhandled error:", error);
    
    // Log to monitoring service
    // sendToSentry(error);
    
    return {
      status: 500,
      body: { message: "Internal server error" },
    };
  },
});

Response Types

Success Responses

Return successful responses with the correct status and body:
return { status: 200, body: todo };
return { status: 201, body: newTodo };
return { status: 204 }; // No content

Error Responses

Return error responses that match your contract:
return { status: 400, body: { message: "Invalid input" } };
return { status: 401, body: { message: "Unauthorized" } };
return { status: 404, body: { message: "Not found" } };

Next.js Integration

App Router

// app/api/todos/[id]/route.ts
import { server } from "@/lib/server";
import { getTodo } from "@/contracts/todo";

export const GET = server.route(getTodo).handle(async ({ path }) => {
  // Implementation
});

Pages Router

// pages/api/todos/[id].ts
import { server } from "@/lib/server";
import { getTodo } from "@/contracts/todo";

export default server.route(getTodo).handle(async ({ path }) => {
  // Implementation
});

Best Practices

Move business logic to use cases or services. Handlers should orchestrate, not implement.
Always access external dependencies through ports to keep your code testable.
Define comprehensive validation in your contracts, not in your handlers.
Return appropriate status codes and error messages that match your contract.
Use structured logging through a provider for debugging and monitoring.

Next Steps