pattern-architect-api-endpoint
Skill0 starsChanged 5 months agoMITView on GitHub
Resource-oriented REST design — the single authority for API endpoint shape decisions (paths, verbs, request / response body, status codes, error envelope, pagination, sorting, filtering, internal-vs-public surface, versioning, idempotency policy, rate-limit policy, trailing-slash spelling). Endpoints are internal-by-default under `/_internal`; public, versioned surface is opt-in per project. Activate when designing, adding, or refactoring an HTTP endpoint, controller, or handler. Every decision lands in the api contract at `docs/api-contract/<entity>.yaml` which engineers implement against and reviewers verify.
In this repository74 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- create-bug-issueSkill
- create-enhancement-issueSkill
- create-feature-issuesSkill
- dream-summary-memorySkill
- memory-conventionSkill
- operation-gitSkill
- pattern-architect-data-modelSkill
- pattern-architect-deep-moduleSkill
- pattern-e2e-coding-standardSkill
- pattern-engineer-apiSkill
- pattern-engineer-backend-standardSkill
- pattern-engineer-coding-standardSkill
- pattern-engineer-containerSkill
- pattern-engineer-databaseSkill
- pattern-engineer-fastapiSkill
- pattern-engineer-frontend-standardSkill
- pattern-engineer-goSkill
- pattern-engineer-javaSkill
- pattern-engineer-kotlinSkill
- pattern-engineer-nodeSkill
- pattern-engineer-non-functionalSkill
- pattern-engineer-observabilitySkill
- pattern-engineer-pythonSkill
- pattern-engineer-rustSkill
- pattern-engineer-securitySkill
- pattern-engineer-ssrSkill
- pattern-engineer-swiftSkill
- pattern-engineer-typescriptSkill
- pattern-engineer-viteSkill
- pattern-exploratory-testingSkill
- pattern-reviewer-apiSkill
- pattern-reviewer-backend-standardSkill
- pattern-reviewer-coding-standardSkill
- pattern-reviewer-containerSkill
- pattern-reviewer-contractSkill
- pattern-reviewer-databaseSkill
- pattern-reviewer-fastapiSkill
- pattern-reviewer-frontend-standardSkill
- pattern-reviewer-goSkill
- pattern-reviewer-javaSkill
- pattern-reviewer-kotlinSkill
- pattern-reviewer-nodeSkill
- pattern-reviewer-non-functionalSkill
- pattern-reviewer-observabilitySkill
- pattern-reviewer-pythonSkill
- pattern-reviewer-rustSkill
- pattern-reviewer-securitySkill
- pattern-reviewer-ssrSkill
- pattern-reviewer-swiftSkill
- pattern-reviewer-test-coverageSkill
- pattern-reviewer-typescriptSkill
- pattern-reviewer-viteSkill
- pattern-test-coverageSkill
- principle-engineer-tddSkill
- scaffold-projectSkill
- workflow-architect-interviewSkill
- workflow-designer-sample-pagesSkill
- workflow-design-interviewSkill
- workflow-e2e-authorSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 4 are distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈4
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Do NOT activate for: internal function/method design that is not exposed over HTTP, GraphQL/gRPC schema work (those have their own …
- Very specific: line 88
When fields is absent, return the documented default field set (not necessarily everything — e.g., heavy nested objects can require …
- Distinctive: line 92
Every non-2xx response uses a single canonical shape:
- Distinctive: line 125
Use cursor-based pagination only when offset is known to break: very large datasets, append-heavy feeds, or strict ordering under …
- Very specific: line 127
Filtering — comparison operators: use a field[op]=value suffix syntax for ranges and inequalities:
- Very specific: line 141
Every internet-reachable endpoint is rate-limited — both /_internal and public surfaces, since "internal" denotes the ownership/versioning …
- Distinctive: line 158
Every endpoint is one of two surfaces.
- Very specific: line 173
Default to no trailing slash on resource and item paths (/users, /users/{id}).
- Very specific: line 177
Every decision this skill produces lands in docs/api-contract/<entity>.yaml — one file per API resource.
- Very specific: line 199
If the contract is missing for an endpoint the engineer is about to touch, this skill — not the engineer — owns adding it.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
pattern-architect-api-endpoint
The authority for HTTP endpoint shape. Every new or changed endpoint flows through this skill first; the decisions made here are recorded in the api contract, which engineers implement against and reviewers verify. Endpoint shape is decided here, not re-litigated in code, commit messages, or PR descriptions.
When to activate
Activate this skill whenever the user:
- asks to design, add, scaffold, expose, or refactor an HTTP endpoint, route, controller, or handler
- asks "what URL / verb / status code should this be?", "how should I structure this route?", or similar framing questions
- asks for a review of a controller, route file, or API design doc
- is wiring a new endpoint in Express, Fastify, NestJS, FastAPI, Flask, Django REST, Rails, Gin, Echo, or similar HTTP frameworks
Do NOT activate for: internal function/method design that is not exposed over HTTP, GraphQL/gRPC schema work (those have their own conventions), pure client-side fetch refactors that don't change the contract, or generic "fix this bug" requests where the endpoint shape is incidental.
Pattern
The canonical shape is resource-oriented REST. Frame the endpoint as <verb> <resource path>, then fill in request/response, errors, list-query params, surface (internal/public), and idempotency. Endpoints are internal by default and served under the /_internal prefix with no version segment — public, versioned surface is opt-in (see Surface & versioning).
# Internal surface is the default — under /_internal, no version prefix
# Collection
GET /_internal/orders → 200 list (paginated)
POST /_internal/orders → 201 created (Idempotency-Key supported)
# Item
GET /_internal/orders/{order_id} → 200 | 404
PATCH /_internal/orders/{order_id} → 200 | 404 | 409
DELETE /_internal/orders/{order_id} → 204 | 404
# Sub-resource (one level of nesting max)
GET /_internal/orders/{order_id}/items
POST /_internal/orders/{order_id}/items
# Action that doesn't fit CRUD — last resort
POST /_internal/orders/{order_id}:cancelURL & verb rules
- Internal by default, under
/_internal. An endpoint is internal unless a project scenario explicitly exposes it to external/third-party consumers. Serve internal paths under the/_internalprefix with no version segment (/_internal/orders). Promoting an endpoint to public surface is a deliberate, opt-in decision — see Surface & versioning. - Resources are plural nouns, kebab-case:
/order-items, not/orderItemor/order_itemor/getOrders. - No verbs in paths. The HTTP method is the verb. If an action genuinely doesn't fit CRUD, use a
:actionsuffix (/orders/{id}:cancel) — and only after trying to model it as a state change via PATCH first. - Verb selection:
POST— create on a collection, or non-idempotent action. Returns201withLocationheader on create.PUT— full replacement. Use sparingly —PATCHis usually right.PATCH— partial update; should be idempotent when possible.DELETE—204on success,404if already gone (not an error worth surfacing twice).
- Nesting max one level deep.
/orders/{id}/itemsis fine;/users/{id}/orders/{id}/items/{id}is not — flatten by linking via query (/items?order_id=…) or top-level resources.
Request & response shape
- Field naming: snake_case in JSON bodies and query params. Pick one convention and never mix.
- IDs are opaque strings, not integers, even if the DB uses integers. Future-proofs against migration.
- Single-item response is the bare object — no
{"data": {...}}envelope for single resources. - List response uses a uniform envelope with pagination metadata:
{
"data": [ {...}, {...} ],
"pagination": {
"page": 2,
"per_page": 50,
"total": 1234,
"total_pages": 25
}
}Sparse fieldsets
Let clients ask for only the fields they need via a fields query param — saves bandwidth and lets one endpoint serve list-card and detail views.
GET /_internal/orders?fields=id,status,total_amount
GET /_internal/orders/{id}?fields=id,customer,items.sku,items.quantity- Comma-separated field names; dot-notation for nested fields.
- Server validates against an allow-list — unknown fields →
400withcode: "unknown_field". idis always returned even if omitted. Pagination metadata is unaffected.- When
fieldsis absent, return the documented default field set (not necessarily everything — e.g., heavy nested objects can require explicit opt-in).
Error format
Every non-2xx response uses a single canonical shape:
{
"error": {
"code": "order_not_found",
"message": "No order with id 'ord_abc123'",
"details": { "order_id": "ord_abc123" }
}
}codeis a stable, snake_case, machine-readable string. Clients branch on this, never onmessage.messageis human-readable; can change without a breaking-change bump.- HTTP status codes carry meaning — the status code is part of the contract.
Never leak internals on 5xx
A 5xx response must be opaque to the client. Detailed diagnostics stay on the server.
Catch unhandled exceptions at the framework boundary and return a generic body:
{ "error": { "code": "internal_error", "message": "An internal error occurred.", "request_id": "req_a1b2c3" } }- Never include stack traces, SQL fragments, file paths, library names, env values, or upstream error text in the response.
- Always log the full exception (stack trace, request context, user/tenant ID) server-side, keyed by the same
request_idreturned to the client. Support usesrequest_idto correlate. - Validation/expected errors are
4xxwith specificcodevalues. Reserve5xxfor genuinely unexpected server faults.
Pagination, filtering, sorting
- Pagination is offset/page-based by default. Query params:
?page=2&per_page=50. Capper_pageserver-side (e.g., 100). Returntotalandtotal_pagesso clients can render page controls.- Use cursor-based pagination only when offset is known to break: very large datasets, append-heavy feeds, or strict ordering under concurrent writes. Document the choice on those endpoints explicitly.
- Filtering — simple equality uses flat query params matching response field names:
?status=open&customer_id=cus_123. Multiple values via repeated keys (?status=open&status=pending) or comma-separated — pick one and document it. Filtering — comparison operators use a
field[op]=valuesuffix syntax for ranges and inequalities:?created_at[gte]=2026-01-01&created_at[lt]=2026-02-01 ?total_amount[gt]=100&total_amount[lte]=500 ?status[in]=open,pending ?name[like]=acmeSupported ops:
eq(default, no suffix),ne,gt,gte,lt,lte,in,nin,like. Server validates allow-listed (field, op) pairs — reject unknown combinations with400code: "unsupported_filter".- Sorting:
?sort=created_atascending,?sort=-created_atdescending. Multiple keys via comma:?sort=-created_at,id. Allow-list sortable fields server-side.
Rate limiting
Every internet-reachable endpoint is rate-limited — both /_internal and public surfaces, since "internal" denotes the ownership/versioning contract, not network isolation. Defaults live at the gateway/middleware layer; expensive endpoints can declare tighter limits.
- Identity for the limit key, in priority order: API key → authenticated user/tenant ID → IP address. Anonymous endpoints fall back to IP but should be rare.
- Default budgets (tune per endpoint): read endpoints 600 req/min per user, write endpoints 60 req/min per user, auth/login 10 req/min per IP. Bulk/expensive endpoints (exports, search) get their own bucket.
Always emit these response headers, on success and on
429:RateLimit-Limit: 600 RateLimit-Remaining: 412 RateLimit-Reset: 37- On exhaustion →
429 Too Many Requestswith aRetry-After: <seconds>header and the standard error body (code: "rate_limited"). - Token-bucket or sliding-window — not fixed-window — so a burst at the boundary doesn't double the budget.
Surface & versioning
Every endpoint is one of two surfaces. Internal is the default; public is opt-in per project.
- Internal (default) —
/_internal, unversioned. Unless a scenario explicitly exposes the endpoint to external/third-party consumers, it is internal. Serve it under/_internalwith no version segment (/_internal/orders). You own both client and server, so internal endpoints evolve in lockstep — there is no version contract to maintain, and they can be changed or removed in step with their callers. This does not waive the api contract: every internal endpoint still gets adocs/api-contract/<entity>.yamlentry. The contract is what locks the requirement down and lets backend and frontend build against the same shape — it is the lockstep mechanism, not paperwork you skip because both sides are yours. - Public (opt-in) — versioned. Promote an endpoint to public only when a project scenario requires external consumers. Public endpoints drop the
/_internalprefix and gain URL-prefix versioning:/v1/...,/v2/.... Bump the major version only on breaking changes. Enabling public surface is a per-project decision recorded in the contract (and usually an ADR), not a default. - Additive changes (new optional fields, new endpoints, new enum values that clients can ignore) do not bump the version. Document them in the changelog.
- Deprecate, don't delete on public endpoints: when retiring a
/v1endpoint, return aDeprecationresponse header and announce a sunset date before removing it.
Idempotency & safety
POST(create) accepts anIdempotency-Keyheader. Server stores(key, response)for ≥24h and replays the same response on retry. Required for any endpoint that takes payment, sends a message, or otherwise has visible side effects.- Concurrency control on PATCH/PUT: support
If-Match: <etag>when stale-write conflicts matter; respond412 Precondition Failedon mismatch.
Trailing-slash spelling
- Pick one spelling per path and pin it in the contract.
/meand/me/are different URLs; framework default redirect-to-trailing-slash returns a 307 that dropsSet-Cookieon cross-site responses. - Default to no trailing slash on resource and item paths (
/users,/users/{id}); document the exception when a path genuinely needs one.
Recording the decision in the api contract
Every decision this skill produces lands in docs/api-contract/<entity>.yaml — one file per API resource. The contract is the single source of truth that engineers implement against and reviewers verify; do NOT decide endpoint shape in code, in commit messages, or in PR descriptions.
Internal endpoints are not exempt. The contract is required for every endpoint regardless of surface — it locks the requirement down and serves as the shared reference both backend and frontend build against. The internal/public distinction changes the path shape (/_internal + unversioned vs. versioned) and the deprecation obligation, never whether a contract exists.
What the contract file declares, per endpoint:
- Path (with trailing-slash spelling pinned) + HTTP verb.
- Request body schema (field names, types, constraints, required vs optional).
- Query / path params with constraints + allow-listed filter operators and sortable fields.
- Response body schema (default field set; sparse-fieldset rules; pagination shape on list endpoints).
- Status codes for each outcome (success, validation failure, not found, conflict, unprocessable, rate-limited, server error).
- Error envelope shape + the
codevalues this endpoint can emit. - Idempotency-Key policy (required / supported / not applicable).
- Rate-limit budget + the identity key (API key / user / IP).
- Concurrency / ETag policy when stale writes matter.
- Surface — internal (default, under
/_internal, unversioned) or public (opt-in, versioned) — plus versioning + deprecation notes when the surface is public.
Cross-references:
- Data model decisions (table names, column types, constraints, indexes) live in the sibling
docs/data-model/<entity>.yaml. - Project-wide axes that span every endpoint (error-envelope spelling, rate-limit defaults, idempotency-key TTL) live in the relevant ADR under
docs/ADRs/; this contract inherits them rather than redefining.
If the contract is missing for an endpoint the engineer is about to touch, this skill — not the engineer — owns adding it. The engineer halts and surfaces "no api contract for <endpoint>" rather than guessing.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

