Ports

Ports are the dependency interface your application code uses. They keep handlers and use cases independent from infrastructure choices such as databases, caches, mailers, queues, auth systems, and external APIs.

bun add @contract-kit/ports

Define ports

Use definePorts to capture the exact shape of your dependencies.

import { definePorts } from "@contract-kit/ports";

type Todo = { id: string; title: string; completed: boolean };

export const ports = definePorts({
  db: {
    todos: {
      findById: async (id: string): Promise<Todo | null> => {
        return db.todos.findById(id);
      },
      create: async (data: { title: string }): Promise<Todo> => {
        return db.todos.create(data);
      },
    },
  },
  cache: {
    get: async (key: string) => redis.get(key),
    set: async (key: string, value: string, ttlSeconds?: number) => {
      await redis.set(key, value, ttlSeconds);
    },
    del: async (key: string) => redis.del(key),
    exists: async (key: string) => redis.exists(key),
  },
});

export type AppPorts = typeof ports;

Put ports in context

The server passes ports into createContext. From there, handlers and hooks receive them through ctx.ports.

export const server = await createNextServer({
  ports,
  createContext: ({ ports, req }) => ({
    requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(),
    ports,
  }),
});
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 };
});

Use ports in use cases

Use cases stay framework-agnostic because they receive the same context shape that handlers use.

const createTodo = define
  .command("createTodo")
  .input(CreateTodoSchema)
  .output(TodoSchema)
  .run(async ({ input, ctx }) => {
    const todo = await ctx.ports.db.todos.create(input);
    await ctx.ports.cache.del("todos:list");
    return todo;
  });

Mock ports in tests

Tests can pass plain objects instead of production infrastructure.

const testPorts = definePorts({
  db: {
    todos: {
      findById: async (id: string) => ({ id, title: "Test", completed: false }),
      create: async (data: { title: string }) => ({
        id: "1",
        completed: false,
        ...data,
      }),
    },
  },
  cache: {
    get: async () => null,
    set: async () => {},
    del: async () => 0,
    exists: async () => false,
  },
});

Ports vs providers

Ports are the interface. Providers are startup-time adapters that install ports for production use.

Use direct ports when the dependency is simple or test-local. Use providers when the dependency needs configuration, startup, teardown, or reusable packaging.