- Home
- Skills
- APIs & Backend
- API Design — Contract-First REST, Errors & Versioning
Works with the AI Agents you already use
API Design — Contract-First REST, Errors & Versioning
Contract-first HTTP API design and review: resources, status codes, pagination, errors, idempotency, versioning.
Free
API Design — Contract-First REST, Errors & Versioning
Example session with this skill installed
We're about to build the orders API for our shop. Mobile app and two partner integrations will consume it. Can you review this draft before we write any code?
POST /api/createOrder
body: { "userId": 123, "items": [{ "sku": "TSHIRT-M", "qty": 2 }], "cardToken": "tok_9f..." }
-> 200 { "success": true, "order": { "id": 48213, "user_id": 123, "status_cd": 1, "total": 59.8 } }
GET /api/getOrders?userId=123&apiKey=sk_live_...
-> 200 [ ...every order the user ever placed... ]
POST /api/orders/cancel
body: { "orderId": 48213 }
Errors: always 200 with { "success": false, "error": "<message>" }
IDs are the Postgres auto-increment primary keys.
- Read your context and instructions
- Compiled the api design
Summary. REST fits here. The consumers are a mobile app and outside partners, and the work is resource CRUD over orders, so broad client reach matters more than flexible queries. The draft has 9 problems. Six of them, marked blocker, would be expensive or unsafe to fix after partners integrate. Nothing has shipped yet, so every fix below is free now. The same changes on a live API would be breaking and would need a new version.
Findings
| # | Severity | Issue | Rule | Fix |
|---|---|---|---|---|
| 1 | Blocker | apiKey in the query string | No secrets in URLs (they end up in logs, history and Referer) | Send Authorization: Bearer <token> in a header |
| 2 | Blocker | userId is supplied by the client in the body and the query | Object-level authorization (BOLA, OWASP API #1) | Get the caller from the validated token. GET /orders/{id} must check that the order belongs to the caller |
| 3 | Blocker | POST /api/createOrder has no idempotency key | Writes must be safe to retry | Require an Idempotency-Key header, store the first response and replay it on retry |
| 4 | Blocker | GET /api/getOrders returns every order | Paginate every list from day one | Cursor pagination with page_size (default and hard max) and page_token |
| 5 | Blocker | Errors return 200 with success:false | Status code carries the outcome | RFC 9457 application/problem+json with the matching status and a stable code |
| 6 | Blocker | Auto-increment IDs (48213) are exposed | Opaque IDs | Opaque string IDs (ord_…). Clients must not parse them |
| 7 | Major | Verbs in paths (createOrder, getOrders, cancel) | Resources are nouns | POST /orders, GET /orders, POST /orders/{id}/cancellations |
| 8 | Major | DB columns leak into the response (status_cd, user_id) and money is a float | Don't expose internal models | A deliberate DTO: status as a documented enum, money in integer minor units plus currency |
| 9 | Minor | No versioning, and /api prefix is used with mixed casing | Consistent conventions, explicit versioning | /v1/ path versioning, kebab-case paths, one casing for JSON fields |
Proposed contract (OpenAPI excerpt, write this before the handlers)
openapi: 3.1.0
info: { title: Orders API, version: "1" }
paths:
/v1/orders:
post:
summary: Create an order for the authenticated customer
parameters:
- { name: Idempotency-Key, in: header, required: true, schema: { type: string, maxLength: 255 } }
requestBody:
content:
application/json:
schema: { $ref: "#/components/schemas/CreateOrder" }
responses:
"201": { description: Created, headers: { Location: { schema: { type: string } } } }
"401": { $ref: "#/components/responses/Problem" }
"409": { $ref: "#/components/responses/Problem" }
"422": { $ref: "#/components/responses/Problem" }
get:
summary: List the authenticated customer's orders
parameters:
- { name: page_size, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 20 } }
- { name: page_token, in: query, schema: { type: string } }
- { name: status, in: query, schema: { $ref: "#/components/schemas/OrderStatus" } }
responses:
"200": { description: A page of orders with next_page_token }
/v1/orders/{order_id}:
get:
responses:
"200": { description: The order (only if it belongs to the caller), headers: { ETag: { schema: { type: string } } } }
"404": { $ref: "#/components/responses/Problem" }
/v1/orders/{order_id}/cancellations:
post:
parameters:
- { name: If-Match, in: header, required: true, schema: { type: string } }
responses:
"201": { description: Cancellation recorded }
"409": { description: Order can no longer be canceled (already shipped) }
"412": { description: Order changed since it was read }
"428": { description: If-Match missing }
CreateOrder holds items[] (sku, qty with a max), currency and a payment_method_id. It has no user_id, and fields that aren't on the allow-list are rejected, which blocks mass assignment.
Error envelope (every error path)
{
"type": "https://api.example.com/errors/out-of-stock",
"title": "Item out of stock",
"status": 422,
"detail": "SKU TSHIRT-M has 1 unit left, 2 requested.",
"code": "OUT_OF_STOCK",
"errors": [{ "field": "items[0].qty", "code": "EXCEEDS_STOCK" }],
"traceId": "0af7651a..."
}
Stack traces, SQL and class names are logged on the server under traceId and are never returned to the client.
Decisions to confirm
Card data. cardToken in the order body suggests payment happens inside order creation. If you keep it, the idempotency key is what prevents a double charge on retry. Confirm which provider token format you are accepting.
Cancel as a sub-resource or a state change. POST /orders/{id}/cancellations keeps a record of who canceled and why. PATCH /orders/{id} {"status":"canceled"} also works if no audit record is needed.
Rate limiting. Return 429 with Retry-After per token, and use an explicit CORS allow-list if a web client is ever added.
Next steps
- Write the full
openapi.yamlfrom the excerpt and review it with the mobile and partner teams before any handler exists. - Add a spec linter (for example Spectral) to CI so drift between spec and code fails the build.
- Implement handlers against the spec. Put the per-order ownership check in one place that every
/{order_id}route goes through. - Learning capture: nothing project-specific came up beyond the defaults, so no
learnings/entry was written.
Connects securely to your tools. The creator never sees your data.
About this skill
API Design is a workflow skill for designing a new API or reviewing a change to one. It treats the API as a contract that has to keep evolving without breaking the clients that already depend on it.
Problems it solves
- Verbs in paths (
POST /createUser),200 OKwith an error in the body, and casing that changes from one endpoint to the next. - List endpoints that return everything, and pagination added later in a way that silently breaks clients.
- POST requests that create duplicates when a client retries after a timeout.
- Missing object-level authorization (BOLA, OWASP API #1), secrets in URLs, and stack traces in error responses.
How it works. It is an 11-step workflow, and each step ends with a check you can verify. You choose the protocol on purpose (REST, GraphQL or gRPC), write the OpenAPI spec before the code, model resources as nouns, use HTTP methods and status codes as specified, and classify every change as additive or breaking. Errors use RFC 9457 problem+json with a stable code. Every list is paginated by cursor from day one. Writes are safe to retry with Idempotency-Key and ETag/If-Match. Security is on by default. The skill also lists anti-patterns to reject on sight.
References, loaded only when needed
rest-conventions.md: naming, methods, status codes, pagination, filtering, caching.versioning-and-compatibility.md: versioning strategies, additive vs breaking changes, deprecation withDeprecation/Sunsetheaders.errors-idempotency-concurrency.md: problem+json, idempotency keys, optimistic concurrency, retries, long-running operations, webhooks.security.md: TLS, OAuth2/OIDC, BOLA/BFLA, mass assignment, rate limiting, CORS.
When it triggers. It activates on tasks that touch an API: adding an endpoint, choosing a status code, paginating, versioning, modeling errors, webhooks or OpenAPI. It reads a project learnings/ folder first, and your own conventions there override the defaults.
Free and MIT-licensed. Part of the claude-skills-czl collection.
Source on GitHub: this skill · full repository
How to use
Skills on Agensi run inside your AI agent. Copy the prompt, paste it into your agent, and it connects to Agensi and installs this skill for you. If a payment is needed, your agent shows it to you first.
Reviews
No reviews yet
Be one of the first to try it. Every listed skill passes our trust checks below.
Security scanned
Passed our 8-point scan before listing
Fresh listing
Recently published to Agensi
Free forever
No account required to browse
Trust & safety
Security scanned
Verified clean 11 days ago
- Free to download with an account