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.