playbook/outfitter-agents/plugins/outfitter-stack/skills/stack-patterns/references/handler.md

212 lines
4.5 KiB
Markdown

# Handler Contract
The core abstraction in Outfitter Stack. Handlers are pure functions that accept typed input and context, returning `Result<TOutput, TError>`.
## Signature
```typescript
type Handler<TInput, TOutput, TError extends OutfitterError> = (
input: TInput,
ctx: HandlerContext
) => Promise<Result<TOutput, TError>>;
```
## Type Parameters
| Parameter | Description |
|-----------|-------------|
| `TInput` | Input type (use `unknown` for raw input that needs validation) |
| `TOutput` | Success return type |
| `TError` | Union of possible error types (must extend `OutfitterError`) |
## Handler Structure
```typescript
import {
Result,
ValidationError,
NotFoundError,
createValidator,
type Handler,
} from "@outfitter/contracts";
import { z } from "zod";
// 1. Define input schema
const InputSchema = z.object({
id: z.string().min(1),
options: z.object({
includeDeleted: z.boolean().default(false),
}).optional(),
});
// 2. Create validator
const validateInput = createValidator(InputSchema);
// 3. Define output type
interface UserOutput {
id: string;
name: string;
email: string;
}
// 4. Implement handler
export const getUser: Handler<unknown, UserOutput, ValidationError | NotFoundError> = async (
rawInput,
ctx
) => {
// Validate input
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value;
// Log with context
ctx.logger.debug("Fetching user", { userId: input.id });
// Business logic
const user = await db.users.findById(input.id);
if (!user) {
return Result.err(new NotFoundError("user", input.id));
}
// Return success
return Result.ok(user);
};
```
## Why Handlers?
### Transport Agnostic
Handlers know nothing about:
- CLI flags and arguments
- HTTP headers and status codes
- MCP tool schemas
- WebSocket messages
This separation means one handler serves all transports.
### Testability
Test handlers directly without transport layer:
```typescript
import { createContext } from "@outfitter/contracts";
test("getUser returns user", async () => {
const ctx = createContext({});
const result = await getUser({ id: "user-1" }, ctx);
expect(result.isOk()).toBe(true);
expect(result.value.name).toBe("Alice");
});
```
### Composability
Handlers can call other handlers:
```typescript
const createOrder: Handler<CreateOrderInput, Order, OrderError> = async (input, ctx) => {
// Call another handler
const userResult = await getUser({ id: input.userId }, ctx);
if (userResult.isErr()) {
return Result.err(new ValidationError("Invalid user", { userId: input.userId }));
}
// Continue with order creation
const order = await db.orders.create({
user: userResult.value,
items: input.items,
});
return Result.ok(order);
};
```
### Type Safety
TypeScript knows all possible outcomes:
```typescript
const result = await getUser({ id: "123" }, ctx);
if (result.isOk()) {
// result.value is UserOutput
console.log(result.value.name);
} else {
// result.error is ValidationError | NotFoundError
switch (result.error._tag) {
case "ValidationError":
console.log(result.error.details);
break;
case "NotFoundError":
console.log(result.error.resourceId);
break;
}
}
```
## Validation Pattern
Always validate at handler entry:
```typescript
const handler: Handler<unknown, Output, ValidationError | OtherError> = async (rawInput, ctx) => {
// First: validate
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value; // Now typed!
// Rest of handler uses validated input
};
```
## Context Usage
Access cross-cutting concerns via context:
```typescript
const handler: Handler<Input, Output, Error> = async (input, ctx) => {
// Logging
ctx.logger.info("Processing", { input });
// Request tracing
const requestId = ctx.requestId;
// Configuration
const apiUrl = ctx.config.apiUrl;
// Cancellation
if (ctx.signal.aborted) {
return Result.err(new CancelledError("Operation cancelled"));
}
// Workspace paths
const filePath = path.join(ctx.workspaceRoot, input.filename);
};
```
## Error Handling
Never throw in handlers. Return `Result.err()`:
```typescript
// BAD
if (!user) throw new Error("Not found");
// GOOD
if (!user) return Result.err(new NotFoundError("user", id));
```
Use taxonomy error classes for consistent categorization:
```typescript
import {
ValidationError,
NotFoundError,
ConflictError,
PermissionError,
InternalError,
} from "@outfitter/contracts";
```