REST API Design skill for Codex
Endpoints clients can predict before reading docs. Codex inventories the conventions your API already uses, then designs new endpoints to match them: plural resource paths, a status-code decision table, RFC 9457 error bodies with stable codes, whitelisted filters and sorts, ETag concurrency and 202-based long-running operations. Reviews end with a breaking-change check against the existing contract.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- rest-api-design Installs to ~/.agents/skills/rest-api-design/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install rest-api-design The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project rest-api-design Installs to .agents/skills/rest-api-design/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/rest-api-design.zipunzip -o rest-api-design.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) rest-api-design PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $rest-api-design, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and reviews HTTP/JSON REST APIs - resource naming, methods, status codes, error bodies, filtering and sorting parameters, PATCH semantics, conditional requests and long-running operations - so every endpoint behaves the same way. Use when adding or changing endpoints, writing a new service's public API, choosing between 400/409/422, reviewing route files or controllers for consistency, or when clients complain the API is unpredictable.
Use it when
- Adding endpoints to an HTTP/JSON API
- Standardising status codes and error bodies across a service
- Designing filter, sort and partial-update behaviour
- Reviewing a controller or route file before it ships publicly
Use something else when
- The API is GraphQL rather than resource-based HTTP: use graphql-schema-design
- You only need the error format itself: use problem-details-errors
How to check the result
Call each endpoint with curl -i and confirm the status code, Location/ETag headers and error body match the table; diff the OpenAPI spec for unintended breaking changes.
The mistake it prevents
Returning 200 with an error flag in the body, which breaks client retries, caching and monitoring that all key off the status code.
A good REST API is boring: once a client has used two endpoints, it can predict the URL, method, status code, error shape and filter syntax of the third. Consistency with the API’s existing conventions beats any individual “better” choice.
Workflow
- [ ] 1. Inventory the existing conventions
- [ ] 2. Model resources and relationships
- [ ] 3. Assign methods and status codes
- [ ] 4. Define request/response bodies and the error shape
- [ ] 5. Add query conventions (filter, sort, fields, pagination)
- [ ] 6. Handle concurrency, async work and retries
- [ ] 7. Write the contract and contract tests
1. Inventory before designing
- Read
AGENTS.mdfor API conventions, then any OpenAPI file (openapi.yaml,openapi.json,docs/api/) and two or three existing route handlers. - Record: casing of JSON fields (
camelCasevssnake_case), ID format, timestamp format, envelope or no envelope, error body shape, pagination style, auth scheme. - If conventions conflict across the codebase, follow the newest, most-used pattern and flag the inconsistency instead of inventing a third.
2. Model resources
- Nouns, plural, lowercase, hyphenated:
/invoices,/invoices/{invoiceId}/line-items. - Nest at most one level, and only for true containment. If a child has a global ID,
also expose it top-level:
/line-items/{id}. - IDs are opaque strings to clients, even when they are integers in the database.
Prefer non-sequential IDs (UUIDv7, ULID, prefixed IDs like
inv_...) for public APIs. - Actions that do not map to CRUD become either a sub-resource that is created
(
POST /invoices/{id}/payments) or, as a last resort, a verb sub-path (POST /invoices/{id}:voidor/invoices/{id}/void). Pick one style and keep it.
3. Methods and status codes
| Method | Use for | Success | Idempotent |
|---|---|---|---|
| GET | Read a resource or collection | 200 | yes |
| POST | Create in a collection, or trigger an action | 201 + Location, or 200/202 | no (make it so with idempotency-keys) |
| PUT | Full replacement at a known URL | 200 or 204 | yes |
| PATCH | Partial update | 200 with the updated resource | not inherently |
| DELETE | Remove | 204 (or 202 if async) | yes (repeat returns 204 or 404, pick one) |
Error status decision rules:
| Situation | Code |
|---|---|
| Malformed JSON, wrong types, unknown query param you reject | 400 |
| No or invalid credentials | 401 with WWW-Authenticate |
| Authenticated but not allowed | 403 (or 404 to hide existence) |
| Resource absent, or hidden from this caller | 404 |
| Method not supported on this path | 405 with Allow |
| State conflict: duplicate unique field, invalid state transition | 409 |
If-Match ETag stale | 412 |
Update without required If-Match | 428 |
| Well-formed but fails business validation | 422 |
| Rate or quota exceeded | 429 with Retry-After |
| Unhandled server fault | 500 (never leak stack traces) |
| Upstream dependency failed or timed out | 502 / 504 |
| Planned unavailability, overload | 503 with Retry-After |
Hard rule: never return 200 with {"success": false}. Status codes drive client retries,
caches, proxies and monitoring.
4. Bodies and errors
- Return the resource representation directly; avoid
{ "data": ... }envelopes unless the API already uses them. Collections are the exception: they need room for paging metadata, so use{ "data": [...], "next_cursor": "..." }or the existing equivalent. - Timestamps: RFC 3339 in UTC with
Z(2026-03-01T12:00:00Z). Money: integer minor units plus currency code ({"amount": 1999, "currency": "EUR"}), never floats. - Enums are strings, documented as open sets: clients must tolerate unknown values.
- Omit vs
null: pick one rule for absent optional fields and apply it everywhere. - Error body: use RFC 9457
application/problem+jsonunless the API already has a shape. Always include a stable machine-readable code and per-field errors for validation:
{
"type": "https://api.example.com/problems/validation",
"title": "Request body failed validation",
"status": 422,
"detail": "2 fields are invalid",
"errors": [
{ "pointer": "/email", "code": "invalid_format", "detail": "must be an email address" },
{ "pointer": "/quantity", "code": "out_of_range", "detail": "must be between 1 and 100" }
],
"request_id": "req_01J8Z..."
}
5. Query conventions
- Filtering: plain equality params for common fields (
?status=open&customer_id=cus_1); for ranges use suffixed params (created_after,created_before) or bracket syntax (created[gte]=). Do not accept arbitrary field names that map straight to SQL columns. - Multi-value: repeated params (
?status=open&status=paid) or comma lists; pick one. - Sorting:
?sort=-created_at,name(leading-is descending). Whitelist sortable fields and always append a unique tiebreaker server-side. - Sparse fields / expansion:
?fields=id,totaland?expand=customeronly if clients need them; each expansion is a performance commitment. - Reject unknown filter params with 400 rather than silently ignoring them; silent ignores turn typos into “return everything”.
- Pagination details live in the api-pagination skill; default page size 20-50, hard max.
6. Concurrency, async and retries
- Optimistic concurrency: return
ETagon GET, requireIf-Matchon PUT/PATCH/DELETE for contended resources, answer 412 on mismatch. - PATCH: use JSON Merge Patch (RFC 7396,
application/merge-patch+json) by default;nullmeans “remove”. Use JSON Patch (RFC 6902) only when clients need array ops. - Long-running work: respond
202 AcceptedwithLocation: /operations/{id}; the operation resource exposesstatus(pending|running|succeeded|failed), a result link and an error. Do not hold HTTP connections open for minutes. - Unsafe POSTs that clients retry (payments, orders) need an
Idempotency-Key.
7. Contract and tests
- Update or create the OpenAPI document in the same change as the handler.
- Add at least one test per endpoint for: success shape, auth failure, validation
failure, not found. Assert status,
Content-Type, and errorcode, not message text.
Gotchas
PUTthat silently ignores missing fields is a PATCH; clients will rely on the bug.- Returning 404 for “collection is empty” breaks clients; an empty list is 200.
- Booleans that later need a third state (
is_active) become breaking changes; prefer astatusenum for lifecycle fields. - Case-sensitive IDs combined with case-insensitive routing produce phantom 404s.
- Large integers (IDs, money in minor units over 2^53) lose precision in JavaScript clients; send them as strings.
DELETEwith a request body is ignored by some proxies; put parameters in the path.- CORS preflights fail on non-simple custom headers unless allowed; see cors-configuration.
Verification
- Run the project’s API tests; then exercise each new endpoint with
curl -iand confirm status, headers (Location,ETag,Content-Type) and body shape. - Diff the OpenAPI spec against the base branch and confirm no unintended breaking change (removed field, narrowed type, new required param).
- Grep the handlers for
res.status(200)/return 200paths that wrap errors.
Output template
When proposing or reviewing an API, deliver:
## Endpoint summary
| Method | Path | Purpose | Success | Errors |
|---|---|---|---|---|
| POST | /invoices | Create draft invoice | 201 + Location | 400, 409, 422 |
## Conventions applied
- Casing, IDs, timestamps, money, error format, pagination: <one line each>
## Breaking-change check
- <none | list with migration notes>
## Open questions
- <decisions the API owner must make> What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "REST API Design"
short_description: "Endpoints clients can predict before reading docs"
default_prompt: "$rest-api-design Design the endpoints for invoices and line items in this service" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.