Application
The @contract-kit/application package provides a fluent builder for use cases — the core business operations in your application.
bun add @contract-kit/application
Creating a use case factory
Start by creating a factory scoped to your application's context type:
import { createUseCaseFactory } from "@contract-kit/application";
import type { DomainEventDef } from "@contract-kit/domain";
type Todo = { id: string; title: string; completed: boolean };
type AppCtx = {
ports: {
db: {
todos: {
create: (input: { title: string }) => Promise<Todo>;
findById: (id: string) => Promise<Todo>;
};
};
eventBus: {
publish: <Payload>(
event: DomainEventDef<string>,
payload: Payload,
) => Promise<void>;
};
};
};
const define = createUseCaseFactory<AppCtx>();
Use cases validate their input before the handler runs and validate the returned output before resolving. This makes them safe to call from HTTP routes, jobs, scripts, tests, and event handlers.
const define = createUseCaseFactory<AppCtx>({
validate: true, // default
});
const metadataOnly = createUseCaseFactory<AppCtx>({
validate: false,
});
Commands and queries
Use .command() for operations that change state and .query() for read-only operations:
import { z } from "zod";
const createTodo = define
.command("createTodo")
.input(z.object({ title: z.string() }))
.output(z.object({ id: z.string(), title: z.string(), completed: z.boolean() }))
.run(async ({ input, ctx }) => {
return ctx.ports.db.todos.create(input);
});
const getTodo = define
.query("getTodo")
.input(z.object({ id: z.string() }))
.output(z.object({ id: z.string(), title: z.string(), completed: z.boolean() }))
.run(async ({ input, ctx }) => {
return ctx.ports.db.todos.findById(input.id);
});
Inside .run(...), input is the parsed schema output. Schema defaults, coercions, and transforms have already been applied.
Reusing schemas in contracts
The finalized use case exposes inputSchema and outputSchema as public properties. Reuse them in HTTP contracts when the request body and success response match the application operation:
export const createTodoContract = todos
.post("/api/todos")
.body(createTodo.inputSchema)
.responses({
201: createTodo.outputSchema,
});
Keep explicit contract schemas when the HTTP shape differs from the application input or output.
Emitting domain events
Use cases can declare which domain events they may emit. .emits(...) is metadata; publish the event inside .run(...) via your event bus port:
import { domainEvent } from "@contract-kit/domain";
const todoCreated = domainEvent(
"todo.created",
z.object({ id: z.string(), title: z.string() }),
);
const createTodo = define
.command("createTodo")
.input(z.object({ title: z.string() }))
.output(z.object({ id: z.string(), title: z.string() }))
.emits([todoCreated])
.run(async ({ input, ctx }) => {
const todo = await ctx.ports.db.todos.create(input);
await ctx.ports.eventBus.publish(todoCreated, {
id: todo.id,
title: todo.title,
});
return todo;
});
Instrumentation
Pass an onRun hook to observe use case execution:
const define = createUseCaseFactory<AppCtx>({
onRun(event) {
// event.phase: "start" | "end" | "error"
// event.name, event.kind, event.durationMs
console.log(`[${event.phase}] ${event.name} (${event.durationMs}ms)`);
},
});
Validation failures are reported through the same hook as phase: "error".
Validation errors
Use case validation failures throw UseCaseValidationError:
import { UseCaseValidationError } from "@contract-kit/application";
try {
await createTodo.run({ ctx, input });
} catch (error) {
if (error instanceof UseCaseValidationError) {
error.useCaseName;
error.phase; // "input" | "output"
error.issues;
}
}
Wiring into route handlers
Use cases are called from your route handlers with the current context:
import { createTodo } from "./use-cases";
export const POST = server.route(contracts.createTodo).handle(async ({ ctx, body }) => {
const result = await createTodo.run({ input: body, ctx });
return { status: 201, body: result };
});