Use when designing REST endpoints, defining error envelopes, setting a versioning or deprecation policy, choosing pagination shape, adding idempotency to mutations, reviewing API contracts, or when two services need a stable interface between them.
Scanned 5/28/2026
npx -y skills add aneja5/forge-skills --skill api-design --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Api Design?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/aneja5-api-design)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: api-design
description: Use when designing REST endpoints, defining error envelopes, setting a versioning or deprecation policy, choosing pagination shape, adding idempotency to mutations, reviewing API contracts, or when two services need a stable interface between them.
---
# API Design
## Overview
Define the project's API conventions *before* endpoints proliferate. Output is `.forge/api-design.md` — verbs and status codes, the error envelope schema, versioning + deprecation policy, pagination/filter contract, rate-limiting envelope, auth/authz envelope, idempotency rules for mutations, and the public-vs-internal API boundary. Consumed by `architecture-and-contracts` (per-module contracts inherit these), `incremental-implementation` (endpoints must conform), and `code-review-and-quality` (PR review checklist).
## When to Use
- A new service or new public endpoint is being designed
- Two services need a stable interface and there's no project-wide envelope
- Error shapes vary across endpoints and clients have to special-case each one
- A breaking change is about to ship and there's no versioning policy
- A list endpoint is being added and pagination shape is unclear
- A mutation endpoint will be retried by clients and there's no idempotency story
## When NOT to Use
- A single trivial endpoint is being added to a service that already has documented conventions
- Internal-only RPC inside one service (use module contracts via `architecture-and-contracts`)
- GraphQL or gRPC — adapt the principles but the file should call out the protocol
## Common Rationalizations
| Thought | Reality |
|---------|---------|
| "We'll version when we break something" | By then clients are coupled to the unversioned shape. Versioning costs nothing on day one, costs a migration on day 200. |
| "Every endpoint is different, no need to standardize" | Inconsistency is a bug, not flexibility. Every divergent error shape costs clients a special case. |
| "Just return 200 with an error field" | Clients can't distinguish success from failure without parsing the body. Proxies and CDNs cache 200s aggressively. |
| "Internal APIs don't need contracts" | Internal APIs become external APIs the moment a second team touches them. Two consumers = production interface. |
| "PATCH replaces the resource" | RFC 7396 says PATCH merges. Clients that depend on merge semantics will break silently if you replace. |
| "Idempotency keys are only for payments" | Network retries happen on every endpoint. Without idempotency, retries create duplicates everywhere. |
## Red Flags
- Different error shapes per endpoint (`{ error: "..." }` here, `{ message: "..." }` there, raw string elsewhere)
- No version prefix or header anywhere — first breaking change requires a fork
- `200 OK` with `{ "success": false }` in the body
- `PATCH` that replaces the resource entirely
- `POST /payments` with no idempotency key support
- An "internal" endpoint with no auth, reachable from the public internet
- `OFFSET`/`LIMIT` pagination on a feed that will exceed 10k rows
- A field added to a response is the only change — no version bump, no deprecation notice
## Precedence with architecture contracts
`.forge/contracts/<module>.md` is the **authoritative source** for module boundaries, operations, types, and error cases. `api-design.md` is the **subordinate** that defines how those operations map to HTTP (verbs, paths, envelope shape, error codes, versioning).
**Rule:** If `.forge/contracts/` exists, **read every contract first.** Your API design must be consistent with the operations and error types they define. If a contract specifies `RefundError` with cases `TransactionNotFound | RefundWindowExpired | AlreadyRefunded`, the matching HTTP design must surface those exact codes — no inventing new ones, no collapsing them into a generic 400.
If a contract is missing an operation you need to expose (e.g., the PRD calls for refunds but `PaymentService` contract has no `refund`), do NOT add it to `api-design.md` alone — file `/feedback` targeting the contract so it gets updated upstream. Otherwise the two artifacts will diverge and `forge-sync` will flag a CONFLICT.
If no contracts exist yet, `api-design.md` may define the envelope policy in isolation, but the module boundaries it references will be back-filled when `architecture-and-contracts` runs.
## Core Process
### Step 0: Read existing contracts (if any)
Glob `.forge/contracts/*.md`. For each contract found:
- Extract every operation in `Provides`
- Extract input/output types
- Extract error types and their conditions
Hold these as the source of truth. Every endpoint you design must trace back to a contract operation (or be flagged as new and worth a feedback entry).
### Step 1: Define base conventions
- **Verbs:** `GET` (read), `POST` (create or non-idempotent action), `PUT` (replace), `PATCH` (merge per RFC 7396), `DELETE` (remove).
- **Status codes:** `2xx` success only. `4xx` for client errors. `5xx` for server errors. Never `200 OK` with `error: true`.
- **Resource naming:** plural nouns (`/users/{id}`, `/orders`).
- **Snake or camel:** pick one for the wire format and never mix.
### Step 2: Write the error envelope schema
Every error response uses the same shape. Write it in `.forge/api-design.md`:
```json
{
"error": {
"code": "RESOURCE_NOT_FOUND",
"message": "User not found.",
"field": "user_id",
"request_id": "01HABC...",
"details": {}
}
}
```
- `code` — stable, machine-readable, screaming-snake-case. Clients pattern-match on this, never on `message`.
- `message` — human-readable, non-technical when surfaced to end users.
- `field` — populated for validation errors only.
- `request_id` — always populated; matches the correlation ID from `observability`.
- `details` — optional, structured per error code.
Cross-reference the `error-handling-and-resilience` taxonomy (transient / permanent / user-correctable maps to retryable HTTP codes).
### Step 3: Set versioning and deprecation policy
Choose one mechanism (URI `/v1/`, header `Accept: application/vnd.api+json;version=1`, or query `?version=1`) and apply it everywhere. Document:
- How a version is introduced (always start at `v1`, never `v0`).
- When a version is required to bump (any field removed, renamed, or type-changed; status-code semantics changed; required-vs-optional flip).
- Deprecation window (minimum 6 months for public, 30 days for internal-only).
- Deprecation signaling: `Deprecation: <date>` header + warnings in `details`.
### Step 4: Pagination, filtering, sorting
- Pagination: cursor preferred. Standard shape: `{ items: [], next_cursor: string|null, prev_cursor: string|null }`. `OFFSET`/`LIMIT` only allowed for small admin endpoints.
- Filtering: `?status=active` for exact match, `?status=active,pending` for `OR`, `?created_after=...` for ranges. No nested-query DSL unless the product justifies one.
- Sorting: `?sort=field` ascending, `?sort=-field` descending. Multi-sort: `?sort=-created_at,id`.
### Step 5: Idempotency, rate limiting, auth envelopes
- **Idempotency:** every mutation accepts `Idempotency-Key: <client-generated>`. Server stores result keyed by `(endpoint, key)` for at least 24h. Replays return the original response.
- **Rate limiting:** every response includes `RateLimit-Limit`, `RateLimit-Remaining`, `RateLimit-Reset`. `429` on exceedance with `Retry-After`.
- **Auth:** standard `Authorization: Bearer <token>`. Errors use stable codes (`AUTH_MISSING`, `AUTH_EXPIRED`, `AUTH_INVALID`, `FORBIDDEN`). Never leak existence — `403` and `404` for "exists but you can't see it" both return `404` at the public boundary.
### Step 6: Document the public/internal boundary
In `.forge/api-design.md`: list every endpoint as `public` (versioned, deprecation-aware, hardened) or `internal` (faster iteration, still auth-gated, never reachable from internet). Internal endpoints get their own subdomain or VPC routing. Mixing the two on the same hostname guarantees future leaks.
### Step 7: Header
Prepend a `forge:meta` header (`generated_by: api-design`, `generated_at: <ISO 8601 UTC with Z>`, `depends_on: [.forge/architecture.md]` — paths only, never hashes, `generated_from: {.forge/architecture.md: <upstream content_hash AT generation time>}`, `content_hash: <sha256 first 8 of THIS file's body>`). See [forge-dependency-graph](../../references/forge-dependency-graph.md).
## Verification
- [ ] Existing `.forge/contracts/*.md` read (or noted as absent); every endpoint traces back to a contract operation
- [ ] No endpoint invents an error code that contradicts the contract's named errors
- [ ] If a needed operation is missing from contracts, a `/feedback` entry was filed targeting the contract — not silently added here
- [ ] `.forge/api-design.md` written
- [ ] Every endpoint returns errors in the standard envelope (code, message, request_id)
- [ ] Every endpoint has a version (URI, header, or query) — none unversioned
- [ ] Every mutation supports an `Idempotency-Key` header (or has a documented reason it doesn't need one)
- [ ] Every paginated list uses the standard cursor envelope
- [ ] No `200 OK` with `{ "success": false }` anywhere
- [ ] Every response includes `RateLimit-*` headers and a `request_id`
- [ ] Public-vs-internal labels assigned for every endpoint, with separate hostnames
- [ ] Breaking changes have a documented deprecation window before the cut-over
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!