| name | patterns |
| version | 0.1.0 |
| description | Reference for Outfitter Dev Kit conventions including Result types, Handler contract, Error taxonomy, and transport-agnostic patterns. Use when learning the stack, looking up patterns, understanding @outfitter/* packages, or when "Result", "Handler", "error taxonomy", or "OutfitterError" are mentioned. |
| allowed-tools | Read Grep Glob |
Outfitter Dev Kit Patterns
Primary reference for @outfitter/* package conventions.
Handler Contract
Handlers are pure functions that:
- Accept typed input and context
- Return
Result<TOutput, TError>
- Know nothing about transport (CLI flags, HTTP headers, MCP tool schemas)
type Handler<TInput, TOutput, TError extends OutfitterError> = (
input: TInput,
ctx: HandlerContext
) => Promise<Result<TOutput, TError>>;
Example
import { Result, NotFoundError, type Handler } from "@outfitter/contracts";
export const getUser: Handler<{ id: string }, User, NotFoundError> = async (
input,
ctx
) => {
ctx.logger.debug("Fetching user", { userId: input.id });
const user = await db.users.findById(input.id);
if (!user) {
return Result.err(NotFoundError.create("user", input.id));
}
return Result.ok(user);
};
Why? Testability (just call the function), reusability (same handler for CLI/MCP/HTTP), type safety (explicit types), composability (handlers wrap handlers).
Result Types
Uses Result<T, E> from better-result for explicit error handling.
import { NotFoundError, Result } from "@outfitter/contracts";
const ok = Result.ok({ name: "Alice" });
const err = Result.err(NotFoundError.create("user", "123"));
if (result.isOk()) {
console.log(result.value);
} else {
console.log(result.error);
}
const message = result.match({
ok: (user) => `Found ${user.name}`,
err: (error) => `Error: ${error.message}`,
});
const combined = combine2(result1, result2);
Error Taxonomy
Ten categories map to exit codes and HTTP status:
| Category | Exit | HTTP | When to Use |
|---|
validation | 1 | 400 | Invalid input, schema failures |
not_found | 2 | 404 | Resource doesn't exist |
conflict | 3 | 409 | Already exists, version mismatch |
permission | 4 | 403 | Forbidden action |
timeout | 5 | 504 | Operation took too long |
rate_limit | 6 | 429 | Too many requests |
network | 7 | 502 | Connection failures |
internal | 8 | 500 | Unexpected errors, bugs |
auth | 9 | 401 | Authentication required |
cancelled | 130 | 499 | User interrupted (Ctrl+C) |
import {
ValidationError,
NotFoundError,
getExitCode,
} from "@outfitter/contracts";
ValidationError.create("email", "invalid");
NotFoundError.create("user", "user-123");
getExitCode(error.category);
getStatusCode(error.category);
Validation
Use Zod with createValidator for type-safe validation returning Results:
import { createValidator } from "@outfitter/contracts";
import { z } from "zod";
const InputSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
});
const validateInput = createValidator(InputSchema);
const inputResult = validateInput(rawInput);
if (inputResult.isErr()) return inputResult;
const input = inputResult.value;
Context
HandlerContext carries cross-cutting concerns:
import { createContext } from "@outfitter/contracts";
const ctx = createContext({
logger: myLogger,
config: resolvedConfig,
signal: controller.signal,
workspaceRoot: "/project",
});
| Field | Type | Description |
|---|
requestId | string | Auto-generated UUIDv7 |
logger | Logger | Structured logger |
config | ResolvedConfig | Merged config |
signal | AbortSignal | Cancellation signal |
workspaceRoot | string | Project root |
cwd | string | Current directory |
Bun-First APIs
Prefer Bun-native APIs:
| Need | Bun API |
|---|
| Hashing | Bun.hash() |
| Globbing | Bun.Glob |
| Semver | Bun.semver |
| Shell | Bun.$ |
| Colors | Bun.color() |
| String width | Bun.stringWidth() |
| SQLite | bun:sqlite |
| UUID v7 | Bun.randomUUIDv7() |
References