Skip to main content

Overview

This quickstart guide will help you build your first type-safe API endpoint with Contract Kit using Next.js.

Prerequisites

  • Node.js 18+ or Bun
  • Basic knowledge of TypeScript
  • Familiarity with Next.js (for this guide)
1

Install Dependencies

Install Contract Kit and your preferred schema library:
npm install contract-kit zod
npm install @contract-kit/next next
Or with Bun:
bun add contract-kit zod
bun add @contract-kit/next next
2

Define a Contract

Create a contract that describes your API endpoint:
// app/contracts/todo.ts
import { createContractGroup } from "contract-kit";
import { z } from "zod";

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

export const getTodo = todos
  .get("/api/todos/:id")
  .path(z.object({ id: z.string() }))
  .response(200, z.object({
    id: z.string(),
    title: z.string(),
    completed: z.boolean(),
  }));
3

Set Up Ports (Dependency Injection)

Define your application’s dependencies using ports:
// app/lib/ports.ts
import { definePorts } from "contract-kit";

export const ports = definePorts({
  db: {
    todos: {
      findById: async (id: string) => ({
        id,
        title: "Example Todo",
        completed: false,
      }),
    },
  },
});
4

Create the Server

Initialize the Contract Kit server with your ports:
// app/lib/server.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" },
  }),
});
5

Create the API Route

Use the contract in a Next.js App Router route:
// app/api/todos/[id]/route.ts
import { server } from "@/app/lib/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);
  return { status: 200, body: todo };
});
6

Create a Typed Client

Generate a type-safe client from your contracts:
// app/lib/api-client.ts
import { createClient } from "contract-kit";
import { getTodo } from "@/app/contracts/todo";

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

export const getTodoEndpoint = client.endpoint(getTodo);
7

Use the Client

Make type-safe API calls:
// Anywhere in your app
import { getTodoEndpoint } from "@/app/lib/api-client";

const todo = await getTodoEndpoint.call({
  path: { id: "123" },
});

console.log(todo.title); // Fully typed!

What’s Next?