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 };
});