playbook/outfitter-agents/plugins/outfitter-stack/skills/stack-templates/templates/handler.md

3.9 KiB

Handler Template

Transport-agnostic business logic returning Result<T, E>.

Template

import {
  Result,
  ValidationError,
  NotFoundError,
  createValidator,
  type Handler,
  type HandlerContext,
} from "@outfitter/contracts";
import { z } from "zod";

// ============================================================================
// Input Schema
// ============================================================================

const InputSchema = z.object({
  // Required fields
  id: z.string().min(1, "ID is required"),

  // Optional fields with defaults
  includeDeleted: z.boolean().default(false),

  // Optional fields without defaults
  limit: z.number().int().positive().optional(),
});

type Input = z.infer<typeof InputSchema>;

// ============================================================================
// Output Type
// ============================================================================

interface Output {
  id: string;
  name: string;
  createdAt: Date;
}

// ============================================================================
// Error Types
// ============================================================================

type HandlerErrors = ValidationError | NotFoundError;

// ============================================================================
// Validator
// ============================================================================

const validateInput = createValidator(InputSchema);

// ============================================================================
// Handler Implementation
// ============================================================================

export const myHandler: Handler<unknown, Output, HandlerErrors> = async (
  rawInput,
  ctx
) => {
  // 1. Validate input
  const inputResult = validateInput(rawInput);
  if (inputResult.isErr()) return inputResult;
  const input = inputResult.value;

  // 2. Log entry
  ctx.logger.debug("Processing request", {
    id: input.id,
    requestId: ctx.requestId,
  });

  // 3. Business logic
  const resource = await fetchResource(input.id, {
    includeDeleted: input.includeDeleted,
  });

  if (!resource) {
    return Result.err(new NotFoundError("resource", input.id));
  }

  // 4. Log success
  ctx.logger.debug("Request completed", { id: input.id });

  // 5. Return result
  return Result.ok(resource);
};

// ============================================================================
// Helper Functions (private)
// ============================================================================

async function fetchResource(
  id: string,
  options: { includeDeleted: boolean }
): Promise<Output | null> {
  // Implementation
  return null;
}

Checklist

  • Input validated with createValidator
  • Handler signature includes all error types
  • Uses ctx.logger for logging
  • Returns Result.ok() or Result.err()
  • No thrown exceptions
  • Context passed to nested handlers

Test Template

import { describe, test, expect } from "bun:test";
import { createContext } from "@outfitter/contracts";
import { myHandler } from "../handlers/my-handler.js";

describe("myHandler", () => {
  const ctx = createContext({});

  test("returns success for valid input", async () => {
    const result = await myHandler({ id: "valid-id" }, ctx);

    expect(result.isOk()).toBe(true);
    expect(result.value).toMatchObject({ id: "valid-id" });
  });

  test("returns NotFoundError for missing resource", async () => {
    const result = await myHandler({ id: "missing" }, ctx);

    expect(result.isErr()).toBe(true);
    expect(result.error._tag).toBe("NotFoundError");
    expect(result.error.resourceId).toBe("missing");
  });

  test("returns ValidationError for invalid input", async () => {
    const result = await myHandler({ id: "" }, ctx);

    expect(result.isErr()).toBe(true);
    expect(result.error._tag).toBe("ValidationError");
  });
});