Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design production-grade APIs — REST, GraphQL, gRPC, and WebSocket — with a focus on consistency, versioning, error standards, and developer experience. Use when the user asks to design an API, define endpoints, choose between REST and GraphQL, structure request/response schemas, handle API versioning, design pagination, or produce an OpenAPI/Swagger spec.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 117% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 90% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 376% | 0% |
| case-22 | ✗→✓ | ▲ Improved | 291% | 0% |
Approach every API as a product. The developer calling your API is your user. An API that is hard to understand, inconsistent, or unpredictable is a broken product — even if it technically works.
Design the API before writing a single line of implementation. An API is a contract. Changing it after clients depend on it is expensive. Getting it right upfront is cheap.
Before designing any endpoint, answer these questions:
cancel, approve, publish are better verbs than generic CRUD.| Style | Best for | Avoid when | |-------|----------|------------| | REST | Resource-oriented APIs, public APIs, broad client compatibility, simple CRUD | Complex queries with many relationships, real-time, or highly variable response shapes | | GraphQL | Flexible queries, multiple clients with different data needs, frontend-driven development, deeply nested data | Simple APIs, teams without GraphQL tooling, when over-fetching is not a real problem | | gRPC | High-performance internal service communication, streaming, strongly-typed contracts, polyglot microservices | Browser clients (requires grpc-web proxy), teams unfamiliar with protobuf | | WebSocket | Real-time bidirectional communication (chat, live dashboards, multiplayer) | Request-response patterns that do not need real-time; adds complexity without benefit | | Webhooks | Asynchronous event notification to external systems | When the caller needs to poll or query state; use REST polling or SSE instead |
# Pattern
/{version}/{resource}/{id}/{sub-resource}
# Examples — good
GET /v1/users
GET /v1/users/{userId}
GET /v1/users/{userId}/orders
POST /v1/users
PUT /v1/users/{userId}
PATCH /v1/users/{userId}
DELETE /v1/users/{userId}
# Actions that don't map to CRUD — use sub-resources
POST /v1/orders/{orderId}/cancel
POST /v1/invoices/{invoiceId}/send
POST /v1/users/{userId}/password-resetURL rules:
user-profiles, not userProfiles or user_profiles)/users, not /getUsers)/users, not /user)/v1/) for public APIs — query param or header for internal APIs| Method | Semantics | Idempotent | Safe | |--------|-----------|------------|------| | GET | Retrieve resource(s) | ✅ | ✅ | | POST | Create a new resource or trigger an action | ❌ | ❌ | | PUT | Replace a resource entirely | ✅ | ❌ | | PATCH | Partially update a resource | ❌ (should be) | ❌ | | DELETE | Remove a resource | ✅ | ❌ |
Rule: GET requests must never have side effects. Never use GET to trigger a state change.
Request body — always:
json{ "name": "Jane Doe", "email": "jane@example.com", "role": "admin" }
Single resource response:
json{ "id": "usr_01HXYZ", "name": "Jane Doe", "email": "jane@example.com", "role": "admin", "createdAt": "2026-01-15T10:30:00Z", "updatedAt": "2026-01-15T10:30:00Z" }
Collection response — always wrap in an envelope:
json{ "data": [ { "id": "usr_01HXYZ", "name": "Jane Doe" }, { "id": "usr_02HABC", "name": "John Smith" } ], "pagination": { "cursor": "eyJpZCI6InVzcl8wMkhBQkMifQ==", "hasMore": true, "total": 247 } }
Why an envelope? Adding metadata (pagination, request ID, warnings) to a bare array response is a breaking change. An envelope allows non-breaking additions forever.
camelCase for JSON APIs (firstName, not first_name or FirstName)"2026-01-15T10:30:00Z") — never Unix timestamps in the response bodyisActive, not isNotActive; isEnabled, not isDisabled)"amount": 1999 means $19.99)"status": "IN_PROGRESS")Every API must have one error format used consistently across all endpoints:
json{ "error": { "code": "VALIDATION_ERROR", "message": "Request validation failed", "details": [ { "field": "email", "code": "INVALID_FORMAT", "message": "Must be a valid email address" } ], "requestId": "req_01HXYZ123", "docsUrl": "https://docs.example.com/errors/VALIDATION_ERROR" } }
Rules:
code is a machine-readable string constant — callers switch on this, not on the HTTP statusmessage is human-readable — never put a machine-parseable value heredetails is an array — multiple validation errors in one response, never force callers to fix one error at a timerequestId on every error response — this is how support traces the request in logsdocsUrl for each error code — link to documentation explaining the error and how to fix it| Code | When to use | |------|------------| | 200 OK | Successful GET, PATCH, PUT | | 201 Created | Successful POST that creates a resource | | 204 No Content | Successful DELETE or action with no response body | | 400 Bad Request | Validation error, malformed request | | 401 Unauthorized | Not authenticated | | 403 Forbidden | Authenticated but not authorized for this resource | | 404 Not Found | Resource does not exist | | 409 Conflict | State conflict (duplicate, version mismatch) | | 422 Unprocessable Entity | Semantically invalid request (valid syntax, invalid business logic) | | 429 Too Many Requests | Rate limit exceeded | | 500 Internal Server Error | Unexpected server error | | 503 Service Unavailable | Planned downtime or dependency unavailable |
Never use 200 with an error body. { "success": false, "error": "..." } with a 200 status is a broken API.
GET /v1/users?cursor=eyJpZCI6InVzcl8wMkhBQkMifQ==&limit=20When: Ordered, append-heavy collections (feeds, logs, events). Stable pages even when new items are inserted.
GET /v1/products?page=3&pageSize=20When: Admin UIs where users jump to specific pages. Avoid for large or frequently-updated datasets (items shift as pages load).
GET /v1/orders?afterId=ord_01HXYZ&limit=50When: Database queries on an indexed column where offset queries become slow.
Pagination response fields (always include):
cursor or nextPage — how to get the next pagehasMore (boolean) — whether more results exist after this pagetotal (optional) — total count (expensive on large datasets; omit if not needed)limit — the limit that was applied (echo it back)/v1/users
/v2/usersPros: Explicit, easy to route in proxies/gateways, cacheable. Use for: External/public APIs, mobile app APIs (clients pin to a version).
API-Version: 2026-01-15Pros: Keeps URLs clean; date-based versions are self-documenting. Use for: Internal services, APIs with sophisticated clients.
Non-breaking (safe to add without versioning):
Breaking (requires version bump):
Deprecation policy: Mark deprecated fields with a X-Deprecated-Fields response header and a deprecated note in the OpenAPI spec. Give callers a minimum of 6 months notice before removal.
Every REST API ships with an OpenAPI 3.x spec. No exceptions.
yamlopenapi: 3.1.0 info: title: User Management API version: 1.0.0 description: Manages user accounts and profiles paths: /v1/users/{userId}: get: summary: Get a user by ID operationId: getUserById tags: [Users] parameters: - name: userId in: path required: true schema: type: string responses: '200': description: User found content: application/json: schema: $ref: '#/components/schemas/User' '404': $ref: '#/components/responses/NotFound' '401': $ref: '#/components/responses/Unauthorized' components: schemas: User: type: object required: [id, name, email, createdAt] properties: id: type: string example: usr_01HXYZ name: type: string example: Jane Doe email: type: string format: email createdAt: type: string format: date-time
OpenAPI rules:
operationId on every endpoint — used for SDK generationtags on every endpoint — groups endpoints in documentation$ref for shared schemas — no duplicationexample values — not string, foo, or 123Every public API must implement rate limiting. Communicate it clearly:
Response headers (always include on rate-limited APIs):
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 842
X-RateLimit-Reset: 1737892800
Retry-After: 60 (only on 429 responses)Rate limit strategy:
429 with Retry-After header — never silently drop requests| Mechanism | Use case | |-----------|----------| | Bearer JWT | User-facing APIs, mobile/web clients | | API Key (Authorization: Bearer sk_...) | Server-to-server, third-party integrations | | OAuth 2.0 + PKCE | Third-party access on behalf of a user | | mTLS | High-security internal service communication |
Rules:
Authorization: Bearer <token> header — never query parameters for auth tokensRead openapi-review.md when reviewing or writing an OpenAPI contract. It supplements, but does not replace, the API-specific requirements in this skill.
Before finalising any API design:
Resource & URL Design
Request & Response
Errors
Pagination
hasMore and cursor/nextPage in every paginated responseVersioning & Breaking Changes
Security
Documentation
Other measured skills in the registry, with their headline benchmark lift.