Design and review HTTP/REST APIs in any back-end stack — resource modelling and naming, methods and status codes, RFC 9457 Problem Details errors, cursor vs offset pagination, filtering and sorting, idempotency keys, ETag/If-Match concurrency, versioning and backward compatibility, long-running operations, webhooks, rate limiting, OAuth2 scopes, and an OpenAPI-first workflow with Spectral linting.
When agents use itUse this whenever the user asks to design a new endpoint or API, write or review an OpenAPI/Swagger spec, choose status codes or an error format, add pagination, versioning or idempotency, plan a breaking change, design webhooks or async job endpoints, define rate limits, or asks "what should this URL look like" or "REST vs GraphQL vs gRPC". Also use it when reviewing controllers or route handlers for contract quality, and when deciding whether an event or message fits better than a synchronous API.
Install
Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.
npx skills add AGCO-Global/org-skills --skill api-design -a github-copilot
# or with the org installer (adds .github/skills/api-design):
npx -y github:AGCO-Global/org-skills add skill api-design
Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.
Installs the backend-shared-skills plugin, which bundles all Backend / Shared skills and keeps them updated.
/plugin marketplace add AGCO-Global/org-skills
/plugin install backend-shared-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill api-design -a claude-code
Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.
Installs into .agents/skills/, which Codex reads.
npx skills add AGCO-Global/org-skills --skill api-design -a codex
Installs into Cursor's skills folder.
npx skills add AGCO-Global/org-skills --skill api-design -a cursor
Installs into Gemini CLI's skills folder.
npx skills add AGCO-Global/org-skills --skill api-design -a gemini-cli
Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.
Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.
Skill contents
API Design
An API is a contract you cannot take back: once a client depends on a field, a status code or an ordering, it is yours forever. Design from the consumer's use cases, make the contract explicit in OpenAPI before code, and make every change additive.
1. Choose the style first
Need
Choose
Why
Public/partner API, CRUD-ish resources, cacheable
REST over HTTP + JSON
Universal tooling, HTTP caching, easy to evolve
Many clients needing different shapes of a rich graph (BFF, mobile)
GraphQL
Client-selected fields; budget query cost and depth
Protobuf contracts, HTTP/2, codegen; poor browser fit
Notify others that something happened; no reply needed
Async events (Kafka, SNS/SQS, AMQP)
Decoupled, resilient; document with AsyncAPI
The rest of this skill covers REST; the compatibility, idempotency and auth rules apply to all styles.
2. Resources and naming
Nouns, plural, lowercase kebab-case: /customers/{customerId}/orders. Nest at most one level; beyond that, use top-level resources with filters.
IDs are opaque strings (UUID/ULID), never sequential integers leaked to the public.
JSON fields in one case convention (camelCase is most common); timestamps in RFC 3339 UTC (2026-03-01T12:00:00Z); money as { "amount": "12.50", "currency": "EUR" } (string or integer minor units, never float).
Actions that don't map to CRUD become sub-resources or state changes: POST /orders/{id}/cancellation, not POST /cancelOrder.
Enums are documented as extensible; clients must tolerate unknown values.
3. Methods and status codes
Method
Semantics
Idempotent
Success codes
GET
Read
Yes
200, 304
POST
Create / non-idempotent action
No (make it so with a key)
201 + Location, 202, 200
PUT
Full replace (or create at known URI)
Yes
200, 204, 201
PATCH
Partial update (JSON Merge Patch, RFC 7396)
Not inherently
200, 204
DELETE
Remove
Yes
204, 202
Errors: 400 malformed, 401 unauthenticated, 403 authenticated but not allowed, 404 not found (also for resources the caller may not see, to avoid leaking existence), 409 state conflict, 412 precondition failed, 415, 422 semantic validation, 428 precondition required, 429 rate limited, 500, 503 + Retry-After. Never 200 with an error body.
type is a stable, documented identifier clients branch on; title/detail are for humans. No stack traces or SQL in any environment reachable by clients.
Small, stable sets needing "jump to page N" and a total
?page=3&pageSize=50 → include totalCount only if cheap
Server enforces a default and maximum limit; cursors are opaque and encode the sort key plus a unique tiebreaker.
Filtering: ?status=open&createdAfter=2026-01-01; sorting: ?sort=-createdAt,name. Allow-list sortable/filterable fields so each is backed by an index.
Sparse fieldsets (?fields=id,name) only when payloads are demonstrably heavy.
6. Idempotency and concurrency
POST that creates or charges accepts an Idempotency-Key header: store key + request hash + response for 24 h+; a replay returns the stored response; same key with a different body returns 422; concurrent in-flight duplicate returns 409.
Optimistic concurrency: GET returns ETag; PUT/PATCH/DELETE send If-Match; mismatch → 412; missing when required → 428. This prevents lost updates between clients.
7. Versioning and compatibility
Strategy
Use when
Trade-off
URI major version/v1/…
Public APIs; default
Visible, cache-friendly, coarse
Header / media type Accept: application/vnd.x.v2+json
Fine-grained representation versions
Harder to test and cache
Date-based version header
Many small breaking changes over time
Needs per-version transformation layer
No version, additive evolution only
Internal APIs with known consumers
Requires discipline and consumer contract tests
Non-breaking: adding optional request fields, response fields, endpoints, enum values (if documented as extensible). Breaking: removing/renaming fields, changing types or formats, making optional fields required, tightening validation, changing status codes, defaults or pagination semantics. Breaking changes ship as a new major version; deprecate the old with Deprecation and Sunset headers, a migration guide and usage metrics before removal.
8. Long-running operations
POST /reports → 202 Accepted + Location: /operations/{opId}. GET /operations/{opId} returns { "status": "running|succeeded|failed", "resultUrl": "...", "error": {...} } with Retry-After for polling. Offer a webhook callback for completion when clients shouldn't poll.
9. Webhooks
Sign every payload (HMAC-SHA256 over timestamp + body) and send the signature and timestamp in headers; receivers verify with constant-time comparison and reject stale timestamps (e.g. > 5 min) to stop replays.
At-least-once delivery: include a unique event id for dedup; retry with exponential backoff + jitter for ~24–72 h; dead-letter and let consumers replay.
Thin events (id, type, resource URL) are safer than full payloads; version the event schema.
10. Rate limiting and security
Return 429 with Retry-After; expose limits with RateLimit-Limit/RateLimit-Remaining/RateLimit-Reset (or the IETF RateLimit/RateLimit-Policy draft headers); document per-client quotas.
OAuth 2.x bearer tokens (authorization code + PKCE for users, client credentials for services); fine-grained scopes (orders:read, orders:write) checked per operation, plus object-level ownership checks on every ID.
TLS only; no secrets or tokens in query strings; validate request bodies against the schema; cap body size.
11. OpenAPI-first workflow
Write or change the OpenAPI 3.1 spec first; review it in the PR like code.
Lint with Spectral (plus a house ruleset: naming, Problem Details on every 4xx/5xx, pagination on lists, operationIds, security on every operation).
Detect breaking changes in CI by diffing against the published spec (e.g. oasdiff).
Generate server stubs/clients or validate requests against the spec; publish docs and mocks from it.
Verbs in paths (/getOrders, /createUser) and RPC-over-POST for everything.
200 OK with { "success": false }; bespoke error shapes per endpoint.
Unbounded list endpoints; offset pagination over large, fast-changing tables.
Non-idempotent POST for payments or orders with client retries enabled.
Renaming or removing fields in place "because no one uses it".
Sequential integer IDs and existence leaks via 403 vs 404.
Code-first APIs with a generated spec nobody reviews or lints.
Tokens or PII in URLs; scopes so coarse that every client gets admin.
---
name: api-design
description: Design and review HTTP/REST APIs in any back-end stack — resource modelling and naming, methods and status codes, RFC 9457 Problem Details errors, cursor vs offset pagination, filtering and sorting, idempotency keys, ETag/If-Match concurrency, versioning and backward compatibility, long-running operations, webhooks, rate limiting, OAuth2 scopes, and an OpenAPI-first workflow with Spectral linting. Use this whenever the user asks to design a new endpoint or API, write or review an OpenAPI/Swagger spec, choose status codes or an error format, add pagination, versioning or idempotency, plan a breaking change, design webhooks or async job endpoints, define rate limits, or asks "what should this URL look like" or "REST vs GraphQL vs gRPC". Also use it when reviewing controllers or route handlers for contract quality, and when deciding whether an event or message fits better than a synchronous API.
metadata:
technology: Backend (general)
type: architecture
---
# API Design
An API is a contract you cannot take back: once a client depends on a field, a status code or an ordering, it is yours forever. Design from the consumer's use cases, make the contract explicit in OpenAPI before code, and make every change additive.
## 1. Choose the style first
| Need | Choose | Why |
|---|---|---|
| Public/partner API, CRUD-ish resources, cacheable | **REST over HTTP + JSON** | Universal tooling, HTTP caching, easy to evolve |
| Many clients needing different shapes of a rich graph (BFF, mobile) | **GraphQL** | Client-selected fields; budget query cost and depth |
| Internal service-to-service, low latency, streaming, strict schemas | **gRPC** | Protobuf contracts, HTTP/2, codegen; poor browser fit |
| Notify others that something happened; no reply needed | **Async events** (Kafka, SNS/SQS, AMQP) | Decoupled, resilient; document with AsyncAPI |
The rest of this skill covers REST; the compatibility, idempotency and auth rules apply to all styles.
## 2. Resources and naming
- Nouns, plural, lowercase kebab-case: `/customers/{customerId}/orders`. Nest at most one level; beyond that, use top-level resources with filters.
- IDs are opaque strings (UUID/ULID), never sequential integers leaked to the public.
- JSON fields in one case convention (camelCase is most common); timestamps in RFC 3339 UTC (`2026-03-01T12:00:00Z`); money as `{ "amount": "12.50", "currency": "EUR" }` (string or integer minor units, never float).
- Actions that don't map to CRUD become sub-resources or state changes: `POST /orders/{id}/cancellation`, not `POST /cancelOrder`.
- Enums are documented as extensible; clients must tolerate unknown values.
## 3. Methods and status codes
| Method | Semantics | Idempotent | Success codes |
|---|---|---|---|
| `GET` | Read | Yes | `200`, `304` |
| `POST` | Create / non-idempotent action | No (make it so with a key) | `201` + `Location`, `202`, `200` |
| `PUT` | Full replace (or create at known URI) | Yes | `200`, `204`, `201` |
| `PATCH` | Partial update (JSON Merge Patch, RFC 7396) | Not inherently | `200`, `204` |
| `DELETE` | Remove | Yes | `204`, `202` |
Errors: `400` malformed, `401` unauthenticated, `403` authenticated but not allowed, `404` not found (also for resources the caller may not see, to avoid leaking existence), `409` state conflict, `412` precondition failed, `415`, `422` semantic validation, `428` precondition required, `429` rate limited, `500`, `503` + `Retry-After`. Never `200` with an error body.
## 4. Errors (RFC 9457 Problem Details)
```http
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
{ "type": "https://api.example.com/problems/validation",
"title": "Request validation failed", "status": 422,
"detail": "2 fields are invalid", "instance": "/orders",
"traceId": "4bf92f3577b34da6",
"errors": [{ "pointer": "/items/0/quantity", "detail": "must be >= 1" }] }
```
`type` is a stable, documented identifier clients branch on; `title`/`detail` are for humans. No stack traces or SQL in any environment reachable by clients.
## 5. Collections: pagination, filtering, sorting
| Choose | When | Shape |
|---|---|---|
| **Cursor (keyset)** — default | Large or changing data, feeds, infinite scroll | `?limit=50&cursor=eyJpZCI6...` → `{ "data": [], "nextCursor": "…" }` |
| **Offset** | Small, stable sets needing "jump to page N" and a total | `?page=3&pageSize=50` → include `totalCount` only if cheap |
- Server enforces a default and maximum `limit`; cursors are opaque and encode the sort key plus a unique tiebreaker.
- Filtering: `?status=open&createdAfter=2026-01-01`; sorting: `?sort=-createdAt,name`. Allow-list sortable/filterable fields so each is backed by an index.
- Sparse fieldsets (`?fields=id,name`) only when payloads are demonstrably heavy.
## 6. Idempotency and concurrency
- `POST` that creates or charges accepts an **`Idempotency-Key`** header: store key + request hash + response for 24 h+; a replay returns the stored response; same key with a different body returns `422`; concurrent in-flight duplicate returns `409`.
- Optimistic concurrency: `GET` returns `ETag`; `PUT`/`PATCH`/`DELETE` send `If-Match`; mismatch → `412`; missing when required → `428`. This prevents lost updates between clients.
## 7. Versioning and compatibility
| Strategy | Use when | Trade-off |
|---|---|---|
| **URI major version** `/v1/…` | Public APIs; default | Visible, cache-friendly, coarse |
| Header / media type `Accept: application/vnd.x.v2+json` | Fine-grained representation versions | Harder to test and cache |
| Date-based version header | Many small breaking changes over time | Needs per-version transformation layer |
| No version, additive evolution only | Internal APIs with known consumers | Requires discipline and consumer contract tests |
**Non-breaking:** adding optional request fields, response fields, endpoints, enum values (if documented as extensible). **Breaking:** removing/renaming fields, changing types or formats, making optional fields required, tightening validation, changing status codes, defaults or pagination semantics. Breaking changes ship as a new major version; deprecate the old with `Deprecation` and `Sunset` headers, a migration guide and usage metrics before removal.
## 8. Long-running operations
`POST /reports` → `202 Accepted` + `Location: /operations/{opId}`. `GET /operations/{opId}` returns `{ "status": "running|succeeded|failed", "resultUrl": "...", "error": {...} }` with `Retry-After` for polling. Offer a webhook callback for completion when clients shouldn't poll.
## 9. Webhooks
- Sign every payload (HMAC-SHA256 over timestamp + body) and send the signature and timestamp in headers; receivers verify with constant-time comparison and reject stale timestamps (e.g. > 5 min) to stop replays.
- At-least-once delivery: include a unique event `id` for dedup; retry with exponential backoff + jitter for ~24–72 h; dead-letter and let consumers replay.
- Thin events (`id`, `type`, resource URL) are safer than full payloads; version the event schema.
## 10. Rate limiting and security
- Return `429` with `Retry-After`; expose limits with `RateLimit-Limit`/`RateLimit-Remaining`/`RateLimit-Reset` (or the IETF `RateLimit`/`RateLimit-Policy` draft headers); document per-client quotas.
- OAuth 2.x bearer tokens (authorization code + PKCE for users, client credentials for services); fine-grained scopes (`orders:read`, `orders:write`) checked per operation, plus object-level ownership checks on every ID.
- TLS only; no secrets or tokens in query strings; validate request bodies against the schema; cap body size.
## 11. OpenAPI-first workflow
1. Write or change the OpenAPI 3.1 spec first; review it in the PR like code.
2. Lint with **Spectral** (plus a house ruleset: naming, Problem Details on every 4xx/5xx, pagination on lists, `operationId`s, security on every operation).
3. Detect breaking changes in CI by diffing against the published spec (e.g. oasdiff).
4. Generate server stubs/clients or validate requests against the spec; publish docs and mocks from it.
## 12. Deliverable format
```
## API design review: <API name>
Style decision: REST | GraphQL | gRPC | events — <one-line reason>
| # | Severity | Operation / path | Issue | Recommendation |
|---|---|---|---|---|
| 1 | blocker | POST /payments | No idempotency; retries double-charge | Require Idempotency-Key |
Breaking-change assessment: none | <list + migration plan>
```
Then the proposed contract as an OpenAPI snippet:
```yaml
paths:
/orders:
get:
operationId: listOrders
security: [{ oauth2: [orders:read] }]
parameters:
- { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 50 } }
- { name: cursor, in: query, schema: { type: string } }
responses:
'200': { $ref: '#/components/responses/OrderPage' }
'4XX': { $ref: '#/components/responses/Problem' }
```
## Anti-patterns to reject
- Verbs in paths (`/getOrders`, `/createUser`) and RPC-over-POST for everything.
- `200 OK` with `{ "success": false }`; bespoke error shapes per endpoint.
- Unbounded list endpoints; offset pagination over large, fast-changing tables.
- Non-idempotent `POST` for payments or orders with client retries enabled.
- Renaming or removing fields in place "because no one uses it".
- Sequential integer IDs and existence leaks via `403` vs `404`.
- Code-first APIs with a generated spec nobody reviews or lints.
- Tokens or PII in URLs; scopes so coarse that every client gets `admin`.