From api-first
Use when designing new HTTP endpoints, reviewing endpoint designs for agent-friendliness, or verifying naming, error schema, pagination, and auth conventions
How this skill is triggered — by the user, by Claude, or both
Slash command
/api-first:api-designThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
camelCase, verb+noun: `createProject`, `listFiles`, `deleteUser`, `uploadProjectFile`. Unique across the entire spec. Required on every endpoint — agents use these as function names. Never `handler_post`, `api_v1_projects_post`, or any auto-generated name.
camelCase, verb+noun: createProject, listFiles, deleteUser, uploadProjectFile. Unique across the entire spec. Required on every endpoint — agents use these as function names. Never handler_post, api_v1_projects_post, or any auto-generated name.
| Code | Meaning | Agent action |
|---|---|---|
| 400 | Invalid request syntax | Fix the request — do not retry |
| 401 | Bad/expired credentials | Check code field: TOKEN_EXPIRED → refresh; TOKEN_INVALID → re-authenticate; do not retry the original request |
| 404 | Not found | Check IDs — do not retry |
| 409 | Conflict | Read conflicting_id from response to resolve without extra call — do not retry |
| 422 | Business logic validation | Fix configuration per detail — do not retry |
| 429 | Rate limited | Read Retry-After header (seconds); fall back to 60s if absent; retry |
| 500 | Server error | Retry only if method is safe or endpoint is idempotent (see retry matrix) |
| 502 | Upstream unavailable | Same as 500 |
| 410 | Gone (deliberately removed) | Do not retry; endpoint is permanently removed |
Use 422 (not 400) for business logic failures. 400 is for malformed requests; 422 is for well-formed requests that violate business rules.
| Method | Safe to retry? | Condition |
|---|---|---|
| GET, HEAD, OPTIONS | Always | Reads only — no side effects |
| DELETE | Always | Idempotent by definition |
| PUT | Always | Idempotent by definition |
| POST | Only if x-agent-retryable: true in spec, or Idempotency-Key was sent | Check the spec before retrying |
| POST (non-idempotent) | Never | Retrying creates duplicates |
After max retries (3): surface the error to the user with the request_id. Do not silently swallow.
Retry backoff: parse Retry-After header if present; otherwise use exponential backoff starting at 1s (1s, 2s, 4s).
x-agent-* OpenAPI extensionsAdd to every operation so agents can introspect behaviour from the spec itself:
post:
operationId: createInvoice
x-agent-retryable: false # false = non-idempotent POST; true = safe to retry
x-agent-idempotency: "Idempotency-Key" # header name if supported; omit if not
x-agent-timeout: 30 # seconds; agent aborts and surfaces error if exceeded
Rules:
x-agent-retryablex-agent-idempotency is set only if the endpoint supports an idempotency headerx-agent-timeout is required on every operation/openapi.json is mandatoryEvery service must expose its current OpenAPI spec at /openapi.json without auth. This is how agents discover what the service can do. Not a "nice to have" — without it, agents must be pre-configured or guess at endpoints. Make it part of the server bootstrap, not an opt-in feature.
offset + limit only. No opaque cursors. Every list response uses this envelope:
{"items": [...], "total": integer, "offset": integer, "limit": integer}
Empty results always return the full envelope — never null, never a missing items key:
{"items": [], "total": 0, "offset": 0, "limit": 20}
Bare array responses cannot be evolved without a breaking change. Offset pagination returns inconsistent results on large, frequently-changing datasets — document this constraint in the spec so agents know.
Pagination iteration pattern for agents — document this in docs/agent-integration.md for each paginated endpoint:
offset = 0
items = []
while True:
resp = get("/resources", params={"offset": offset, "limit": 100})
items += resp["items"]
if offset + resp["limit"] >= resp["total"]:
break
offset += resp["limit"]
List endpoints that support filtering use query parameters with predictable naming:
| Pattern | Example | Notes |
|---|---|---|
| Exact match | ?status=active | Field name as param |
| Multiple values | ?status=active,archived | Comma-separated; OR semantics |
| Range | ?created_after=2026-01-01T00:00:00Z&created_before=2026-02-01T00:00:00Z | {field}_after / {field}_before for dates |
| Search | ?q=term | Free-text search across relevant fields |
| Sort | ?sort=created_at / ?sort=-created_at | Prefix - for descending; default sort documented in spec |
Rules:
INVALID_PARAMETER — never silently ignoreparameters section — agents discover filters from the spec, not from trial and errorUse JSON Merge Patch (application/merge-patch+json, RFC 7396) as the default strategy:
null to clear it (only if project uses Option A null convention)operationId: updateProject, patchUser — verb is update or patchpatch:
operationId: updateProject
x-agent-retryable: true
x-agent-timeout: 10
requestBody:
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/ProjectPatch'
Rules:
x-agent-retryable: true.BUSINESS_RULE_VIOLATION.When to use PUT instead: full resource replacement where the client sends the complete object. PUT replaces; PATCH merges. Don't use PUT for partial updates.
All routes require auth except:
/health, /metrics, /version, /openapi.json/login, /token, /oauth/callback, /register and similar (these cannot require the identity they are establishing)Declare auth in securitySchemes (Bearer JWT, API key, or OAuth2 — specify which).
docs/agent-integration.mdexp claim)TOKEN_EXPIRED (refresh available) vs TOKEN_INVALID (re-authenticate from scratch) vs TOKEN_REVOKED vs INSUFFICIENT_SCOPEPOST /token/refresh; else → re-authenticate fullyUse these as the starting set. Extend per domain but stay consistent across services in the same platform.
| Code | When to use | HTTP status |
|---|---|---|
VALIDATION_FAILED | Request body fails schema validation | 400 |
INVALID_PARAMETER | Query or path parameter invalid | 400 |
TOKEN_EXPIRED | JWT or session expired; refresh available | 401 |
TOKEN_INVALID | Token malformed or signature invalid; re-auth | 401 |
TOKEN_REVOKED | Token was valid but explicitly revoked | 401 |
INSUFFICIENT_SCOPE | Token valid but lacks required scope | 403 |
NOT_FOUND | Resource does not exist | 404 |
SLUG_CONFLICT / RESOURCE_CONFLICT | Duplicate resource | 409 |
STATE_CONFLICT | Current state prevents the operation | 409 |
BUSINESS_RULE_VIOLATION | Valid request, violates a domain rule | 422 |
RATE_LIMITED | Too many requests | 429 |
INTERNAL_ERROR | Unexpected server error | 500 |
UPSTREAM_UNAVAILABLE | A dependency is down | 502 |
GONE | Resource deliberately removed | 410 |
Codes are machine-readable — never localized, never user-facing. The detail field is the human/agent-readable explanation.
Consumers use auto-generated clients, not hand-written HTTP calls. Regeneration runs via pre-commit hook; CI verifies clients match the current spec. See api-first-workflow/references/http-tooling.md for setup.
Design consumers to extract only what they need and ignore unknown response fields. Document this expectation in docs/agent-integration.md — minor versions may add new fields; consumers must not break.
Every POST endpoint must document its strategy:
conflicting_id). Set x-agent-retryable: true.x-agent-idempotency: "Idempotency-Key" and x-agent-retryable: true.x-agent-retryable: false. Document clearly. Agents must not retry on 5xx.Every 409 response must include enough data to resolve the conflict without an additional GET. Minimum:
{
"code": "SLUG_CONFLICT",
"detail": "Project with slug 'my-app' already exists",
"conflicting_id": "proj_abc123",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
The field name for the conflicting resource is conflicting_id (or the equivalent per project convention — apply consistently across all services). Document what the agent should do with it.
All timestamp fields use RFC 3339 format (2026-01-01T00:00:00Z). In the OpenAPI schema, mark as type: string, format: date-time. Never return epoch integers or non-standard formats.
Choose one convention per project and apply uniformly:
null — never omitted. Mark optional fields in the schema and include them in required for responses. Agents never need to distinguish "missing" from "null".null identically only if the spec says so.State-changing endpoints document their consistency model if non-obvious:
Every error response must include a correlation ID field. Every success response should include it too. Propagate as X-Request-ID header in both directions. Include in logs.
Field name: choose one (request_id, requestId, correlationId) consistent with your stack's conventions and apply uniformly across all services. The X-Request-ID HTTP header name is independent of the JSON field name.
An endpoint that simply wraps a single DB row operation (CRUD on one table, exposing the row's columns directly) is a sign the module boundary is in the wrong place. See api-first:module-boundaries.
operationId is set, camelCase verb+noun, uniqueinfo.version bumped/openapi.json is exposed without auth (capability discovery)Error componentError component has: machine-readable error code, actionable detail message, correlation ID — consistent naming across all services{"items": [...], "total", "offset", "limit"} envelope — empty state returns full envelope, never nullsecuritySchemes; all routes require auth except observability + authentication endpointsx-agent-retryable setx-agent-timeout setx-agent-idempotency header name set on operations supporting Idempotency-KeyTOKEN_EXPIRED vs TOKEN_INVALID (vs TOKEN_REVOKED, INSUFFICIENT_SCOPE) codes as appropriateconflicting_id (or equivalent) for agent self-resolutionformat: date-time in schemadetail messages are actionable (not just status descriptions)docs/agent-integration.md updated if endpoint behaviour changed (includes pagination pattern, token lifecycle if new); intent→endpoint table updatedapplication/merge-patch+json content type and return the full resourceparameters section with defaults statedx-agent-* extensions — no agents use this"conflicting_id is non-standard — let's just use detail"{"error": "Bad Request"})operationId like handler_post or api_v1_projects_postx-agent-* extensions on POST operationsSOFT REFERENCE: modularity:balanced-coupling — is this endpoint at the right abstraction level?
references/error-schema.md — full Error component schema, error code taxonomy, securitySchemes templates, /openapi.json exposure, FastAPI examplenpx claudepluginhub u-abramchuk/skills --plugin api-firstSearches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.