api-design
Build production-ready HTTP APIs with clean handlers, consistent error envelopes, health checks, CORS, and operational excellence.
What this skill does
# API Design Patterns
## Core Principle
HTTP handlers have exactly one job: **translate between HTTP and your domain**.
```
HTTP Request
|
v
Handler (thin layer)
|-> Validate Input (Zod via framework)
|-> Call Business Logic: fn(args, deps) -> Result
|-> Map Result to HTTP Response
|
v
HTTP Response
```
Handlers should be thin. They don't contain business logic.
## Required Behaviors
### 1. Route Factory Pattern with DI
Each route follows `fn(args, deps)` and uses a factory for dependency injection:
```typescript
// routes/posts/get-post.ts
import { os, ORPCError } from "@orpc/server";
import { z } from "zod";
import type { PostRepository } from "./types";
// Explicit deps type for this route
type GetPostDeps = {
postRepo: PostRepository;
};
// Factory function: creates route with injected deps
export function createGetPost({ deps }: { deps: GetPostDeps }) {
return os
.input(z.object({ postId: z.string().uuid() }))
.output(PostResponse)
.handler(async ({ input }) => {
const post = await deps.postRepo.findById({ id: input.postId });
if (!post) {
throw new ORPCError("NOT_FOUND", {
status: 404,
message: `Post ${input.postId} not found`,
});
}
return post;
});
}
```
### 2. Consistent Error Envelope
All errors MUST use the same JSON shape:
```typescript
const ErrorResponse = z.object({
code: z.string(), // Machine-readable: "NOT_FOUND"
message: z.string(), // Human-readable explanation
requestId: z.string(), // For correlation in logs
details: z.unknown().optional(),
});
function createErrorResponse(
code: string,
message: string,
requestId: string,
details?: unknown
) {
return { code, message, requestId, details };
}
```
**Rule: `ORPCError(code)` must match `ErrorResponse.code`**:
```typescript
// CORRECT - codes match
throw new ORPCError("NOT_FOUND", {
status: 404,
data: createErrorResponse("NOT_FOUND", "User not found", requestId),
});
// WRONG - codes mismatch
throw new ORPCError("BAD_REQUEST", {
data: createErrorResponse("MISSING_FIELD", ...), // Confusing!
});
```
### 3. Standard Error Mapping
Map all error types consistently:
```typescript
const errorToStatus: Record<string, number> = {
NOT_FOUND: 404,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
VALIDATION_FAILED: 400,
CONFLICT: 409,
TOO_MANY_REQUESTS: 429,
SERVICE_UNAVAILABLE: 503,
};
```
### 4. Health and Readiness Endpoints
```typescript
// /health - Liveness (is process running?)
export const health = os.handler(() => ({ status: "ok" }));
// /ready - Readiness (can handle traffic?)
export const ready = os.handler(async () => {
const checks = {
database: await checkDatabase(),
cache: await checkCache(),
};
const allHealthy = Object.values(checks).every(Boolean);
if (!allHealthy) {
throw new ORPCError("SERVICE_UNAVAILABLE", {
status: 503,
data: { status: "not_ready", checks },
});
}
return { status: "ready", checks };
});
```
**Response contract:**
- **200**: `{ status: "ready", checks }`
- **503**: `{ status: "not_ready", checks }`
### 5. X-Request-ID Header (Central Middleware)
Set this header centrally, not in each handler:
```typescript
// In top-level request middleware or response hook
response.headers.set("X-Request-ID", context.requestId);
```
### 6. CORS Configuration
```typescript
new CORSPlugin({
origin: (origin) => {
// No Origin = not a browser request (curl, server-to-server)
// Return null = "don't emit CORS headers" (request proceeds normally)
if (!origin) return null;
// Return origin string to allow, null to deny
return ALLOWED_ORIGINS.has(origin) ? origin : null;
},
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization', 'Idempotency-Key'],
credentials: true,
});
```
### 7. Idempotency for Mutations
```typescript
export const createOrder = os
.input(CreateOrderInput)
.handler(async ({ input, context }) => {
const { req, requestId } = context as AppContext;
const idempotencyKey = req.headers.get('idempotency-key');
if (!idempotencyKey) {
throw new ORPCError("MISSING_IDEMPOTENCY_KEY", {
status: 400,
data: createErrorResponse(
"MISSING_IDEMPOTENCY_KEY",
"Idempotency-Key header required",
requestId
),
});
}
// Check cache for existing result, or process and store
// ...
});
```
### 8. Rate Limiting with Retry-After
```typescript
throw new ORPCError("TOO_MANY_REQUESTS", {
status: 429,
headers: { "Retry-After": "60" }, // Seconds until retry allowed
data: createErrorResponse(
"TOO_MANY_REQUESTS",
"Rate limit exceeded. Try again in 60 seconds.",
requestId
),
});
```
### 9. Graceful Shutdown
```typescript
let isShuttingDown = false;
process.on('SIGTERM', () => {
isShuttingDown = true;
// Wait for in-flight requests, then exit
});
// In handlers or middleware
if (isShuttingDown) {
throw new ORPCError("SERVICE_UNAVAILABLE", {
status: 503,
message: "Server is shutting down",
});
}
```
### 10. Route File Organization
**One file per route** with co-located tests:
```
routes/
├── posts/
│ ├── get-post.ts # Route factory
│ ├── get-post.test.ts # Co-located test
│ ├── list-posts.ts
│ ├── create-post.ts
│ ├── index.ts # Composes postsRouter
│ └── schemas.ts # Shared Zod schemas
└── index.ts # Composes apiRouter
```
**Naming conventions:**
| Operation | File Name | Factory | Router Key |
|-----------|-----------|---------|------------|
| Get one | `get-post.ts` | `createGetPost` | `getPost` |
| List | `list-posts.ts` | `createListPosts` | `listPosts` |
| Create | `create-post.ts` | `createCreatePost` | `createPost` |
**Composition at boundaries:**
```typescript
// routes/posts/index.ts
export function createPostsRouter({ deps }: { deps: PostsRouterDeps }) {
return {
getPost: createGetPost({ deps }),
listPosts: createListPosts({ deps }),
createPost: createCreatePost({ deps }),
};
}
// routes/index.ts
export function createApiRouter({ deps }: { deps: ApiRouterDeps }) {
return {
posts: createPostsRouter({ deps }),
users: createUsersRouter({ deps }),
};
}
```
## Testing Routes
```typescript
import { describe, it, expect, beforeEach } from "vitest";
import { call, ORPCError } from "@orpc/server";
import { mock } from "vitest-mock-extended";
import { createGetPost, type GetPostDeps } from "./get-post";
describe("getPost", () => {
const postId = "550e8400-e29b-41d4-a716-446655440000";
let deps: GetPostDeps;
let getPost: ReturnType<typeof createGetPost>;
beforeEach(() => {
deps = { postRepo: mock() };
getPost = createGetPost({ deps });
});
it("returns post when found", async () => {
deps.postRepo.findById.mockResolvedValue({
id: postId,
title: "Test Post",
});
const result = await call(getPost, { postId });
expect(result.title).toBe("Test Post");
});
it("throws NOT_FOUND when post missing", async () => {
deps.postRepo.findById.mockResolvedValue(null);
try {
await call(getPost, { postId });
expect.fail("Should have thrown");
} catch (error) {
expect(error).toBeInstanceOf(ORPCError);
expect((error as ORPCError).code).toBe("NOT_FOUND");
}
});
});
```
## Quick Reference
| Concern | Pattern | Where |
|---------|---------|-------|
| Input validation | Zod schemas | Framework boundary |
| Error format | Consistent envelope | All error responses |
| Error codes | `ORPCError(code)` = `ErrorResponse.code` | Handler |
| Request ID | Set in middleware | Top-level |
| Health check | `/health` (200) | Liveness |
| Readiness | `/ready` (200/503) | Traffic routing |
| CORS | Plugin with origin callback | Framework config |
| Idempotency | `Idempotency-Key` header | Related in Design
contribute
IncludedLocal-only OSS contribution command center. Auto-refreshes the user's in-flight PR and issue state on invoke so conversations start with full context — no need to brief Claude on what's in flight. Helps the user find issues to contribute to on GitHub, builds per-repo dossiers of what each upstream expects (CLA, DCO, branch convention, AI policy, draft-first, review bots, issue templates), runs deterministic gates before any external action so AI-assisted contributions don't reach maintainers as slop. State is markdown-only: candidate files at ~/.contribute-system/candidates/, repo dossiers at ~/.contribute-system/research/, append-only event log at ~/.contribute-system/log.jsonl. No database, no cloud calls. Use when the user asks about their PRs / issues / contributions, wants to find new work to take on, claim an issue, build/refresh a repo's dossier, or draft a Design Issue or PR. Trigger with "/contribute", "what's my PR status", "find a contribution", "claim issue X", "draft a Design Issue for Y", "refresh dossier for Z".
architectural-analysis
IncludedUser-triggered deep architectural analysis of a codebase or scoped subtree across eight modes — information architecture, data flow, integration points, UI surfaces, interaction patterns, data model, control flow, and failure modes. This skill should be used when the user asks to "diagram this codebase," "map the architecture," "show the data flow," "give me an ERD," "trace control flow," "find the integration points," "verify the layout pattern," "audit the UX architecture," or any similar request whose primary deliverable is mermaid diagrams plus cited reports under docs/architecture/. Dispatches haiku/sonnet sub-agents in parallel for per-mode exploration, then verifies every citation mechanically before any node lands in a diagram. Not for one-off prose explanations of code (use code-explanation) or for high-level system design from scratch (use system-design).
mcp
IncludedModel Context Protocol (MCP) server development and tool management. Languages: Python, TypeScript. Capabilities: build MCP servers, integrate external APIs, discover/execute MCP tools, manage multi-server configs, design agent-centric tools. Actions: create, build, integrate, discover, execute, configure MCP servers/tools. Keywords: MCP, Model Context Protocol, MCP server, MCP tool, stdio transport, SSE transport, tool discovery, resource provider, prompt template, external API integration, Gemini CLI MCP, Claude MCP, agent tools, tool execution, server config. Use when: building MCP servers, integrating external APIs as MCP tools, discovering available MCP tools, executing MCP capabilities, configuring multi-server setups, designing tools for AI agents.
react-native-skia
IncludedDesign, build, debug, and optimise high-polish animated graphics in React Native or Expo using @shopify/react-native-skia, Reanimated, and Gesture Handler. Use when the user wants canvas-driven UI, shaders, paths, rich text, image filters, sprite fields, Skottie, video frames, snapshots, web CanvasKit setup, or performance tuning for custom motion-heavy elements such as loaders, hero art, cards, charts, progress indicators, particle systems, or gesture-driven surfaces. Also use when the user asks for fluid, glow, glass, blob, parallax, 60fps/120fps, or GPU-friendly animated effects in React Native, even if they do not explicitly say "Skia". Do not use for ordinary form/layout work with standard views.
plaid
IncludedProduct Led AI Development — guides founders from idea to launched product. Six capabilities: Idea (discover a product idea), Validate (pressure-test the idea against fatal flaws, problem reality, competition, and 2-week MVP feasibility), Plan (vision intake + document generation), Design (translate image references into a design.md spec), Launch (go-to-market strategy), and Build (roadmap execution). Use when someone says "PLAID", "plaid idea", "help me find an idea", "product idea", "idea from my business", "idea from my expertise", "plaid validate", "validate my idea", "pressure-test", "is this idea good", "find fatal flaws", "validate the problem", "plan a product", "define my vision", "generate a PRD", "product strategy", "plaid design", "design from image", "translate image to design", "create design.md", "extract design tokens", "plaid launch", "go-to-market", "launch plan", "GTM strategy", "launch playbook", "plaid build", "build the app", "start building", or "execute the roadmap".
nextjs-framer-motion-animations
IncludedAdds production-safe Motion for React or Framer Motion animations to Next.js apps, including reveal, hover and tap micro-interactions, whileInView, stagger, AnimatePresence, layout and layoutId transitions, reorder, scroll-linked UI, and lightweight route-content transitions. Use when the user asks to add, refactor, or debug Motion or Framer Motion in App Router or Pages Router codebases, especially around server/client boundaries, reduced motion, LazyMotion, bundle size, hydration, or route transitions. Avoid for GSAP-style timelines, WebGL or 3D scenes, heavy scroll storytelling, or CSS-only effects unless Motion is explicitly requested.