Client

The client gives you a fully typed HTTP client derived from your contracts. No code generation — just TypeScript inference.

Creating a client

import { createClient } from "contract-kit";
import { getTodo, listTodos, createTodo } from "@/app/contracts/todo";

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

export const getTodoEndpoint = client.endpoint(getTodo);
export const listTodosEndpoint = client.endpoint(listTodos);
export const createTodoEndpoint = client.endpoint(createTodo);

Making requests

GET with path parameters

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

console.log(todo.title); // fully typed

GET with query parameters

const result = await listTodosEndpoint.call({
  query: {
    completed: true,
    limit: 10,
    offset: 0,
  },
});

console.log(result.todos); // Todo[]

POST with a body

const newTodo = await createTodoEndpoint.call({
  body: {
    title: "New todo",
    completed: false,
  },
});

console.log(newTodo.id); // string

With custom headers

const todo = await getTodoEndpoint.call({
  path: { id: "123" },
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

Error handling

The client returns the response body on success and throws a ContractError on non-2xx responses.

import { ContractError } from "contract-kit";

try {
  await createTodoEndpoint.call({
    body: { title: "New todo" },
  });
} catch (error) {
  if (error instanceof ContractError) {
    console.error(error.status, error.details);
  }
}

Configuration

Global headers

const client = createClient({
  baseUrl: process.env.NEXT_PUBLIC_API_URL || "",
  headers: () => ({
    "X-Api-Version": "1.0",
  }),
});

Headers can be a function (sync or async) so you can inject tokens dynamically.

Custom fetch

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

Type safety

The client enforces your contract at the type level. TypeScript will catch mistakes before your code runs.

// TypeScript knows exactly what's required
const todo = await getTodoEndpoint.call({
  path: { id: "123" }, // error if missing or wrong type
});

// TypeScript knows the response shape
todo.title;     // string
todo.completed; // boolean

// TypeScript prevents invalid usage
await getTodoEndpoint.call({
  path: { id: 123 }, // type error: id must be string
});