| name | api-design |
| description | You MUST consult this skill when designing or reviewing any API — REST, gRPC, or OpenAPI specs. Also trigger on resource modeling, field naming, pagination, error handling, versioning, custom vs standard methods, or reviewing an existing API for consistency problems — even if the user doesn't mention AEP. NOT for implementation details (framework setup, middleware config) or general software design (see software-design). |
AEP-Compliant API Design
Design APIs following AEP (Google, Microsoft, Roblox, IBM). The payoff: Terraform providers, CLIs, UIs, and MCP servers auto-generate because they can predict the API shape from resource hierarchies.
Core principle: design resource hierarchies (nouns), not procedure collections (verbs).
Designing around caller intent
Design the API surface around what callers actually want, expressed in their vocabulary — not around how the implementation is structured internally. A networking API should expose connect, read_message, write_message, and disconnect. Exposing a "socket" instead leaks a Unix implementation concept that bundles unrelated concerns (files, network, device I/O) into a single abstraction callers never asked for. The right question is: what does the caller need to accomplish, and what are the fewest, most coherent calls that let them do it?
No footgun-by-omission. Nothing should fail silently because the caller forgot to set an obscure flag or initialize an optional field. Mandatory low-level knobs are a design smell; sensible, safe behavior should be the default, with explicit opt-in for deviation.
Separate the escape hatch. Provide raw or low-level bindings as a distinct layer for the rare callers who genuinely need them. The common-case API should not force everyone through low-level machinery. (See software-design's "layered interfaces" for the structural pattern.)
Difficulty is not a virtue. Jonathan Blow argues — with OpenGL and Vulkan as examples — that keeping a hard API because you feel pride in having mastered it is not a design principle; it is a cost imposed on every future caller. Complexity is only justified when it is essential to the problem domain, which is rare.
Design Process
For every API, answer these four questions in order:
- What are the resources (things)?
- What's the parent-child hierarchy?
- What does each resource's schema look like?
- Which standard methods does each resource need?
Before (verb-oriented — hard to auto-generate clients):
POST /v1/createProject
POST /v1/archiveProject
POST /v1/getProjectTasks
After (resource-oriented — predictable, tooling-friendly):
POST /v1/projects # Create
GET /v1/projects/{project_id} # Get
GET /v1/projects # List
PATCH /v1/projects/{project_id} # Update
DELETE /v1/projects/{project_id} # Delete
POST /v1/projects/{project_id}:archive # Custom method (state transition)
GET /v1/projects/{project_id}/tasks # Nested collection
Model a resource hierarchy, not a database schema — the API is a contract, not a DB mirror. Resource relationships must form a DAG (no cycles).
Standard Methods (AEP-130–135)
Prefer standard > batch > custom > streaming. Each step down costs tooling compatibility.
| Method | Verb + Path | Key Rules |
|---|
| Get | GET /v1/{path} | Every resource needs Get so clients can verify mutations |
| List | GET /v1/{parent}/collection | Response array is results, not the type name. Paginate from day one |
| Create | POST /v1/{parent}/collection?id=X | ID is a query param, not body. Support user-specified IDs by default |
| Update | PATCH /v1/{path} | Merge-patch semantics (application/merge-patch+json). Never PUT |
| Delete | DELETE /v1/{path} | Require force: bool when first-class children exist |
| Custom | POST /v1/{path}:verb | State transitions that can't be expressed as field updates |
OperationId naming: {Verb}{Singular} for standard (GetBook, CreateBook), List{Plural} for List (ListBooks).
Conventions That Diverge From Common Practice
These AEP rules are the ones most likely to be missed. Each exists for a specific reason:
results array in List responses — not books or users. A consistent name lets generic clients parse any collection without knowing the resource type.
id as query param on Create — POST /v1/tasks?id=my-task. Keeps the body a clean resource representation. Without user-specified IDs, Create isn't safely retryable and declarative clients (Terraform) can't reconcile state.
PATCH + merge-patch, never PUT — PUT requires the full resource, so adding a new schema field silently becomes breaking (old clients reset it to zero value on every update).
_time suffix on timestamps — create_time, update_time. Never _at, never camelCase. This makes timestamps recognizable across all AEP APIs.
- Permissions before existence — always check auth first (->
403), then existence (-> 404). Reversing this leaks which resources exist to unauthorized callers.
- RFC 9457 errors —
Content-Type: application/problem+json with type, status, title, detail. Dynamic variables in detail must also appear as top-level fields for programmatic access.
- State transitions via custom methods —
:archive, :publish, not PATCH on a state field. State machines need server-side validation that Update can't express.
- Duplicate Create ID ->
409 ALREADY_EXISTS, unless caller can't see the duplicate -> 403 (avoids leaking existence info).
Review Checklist
References
| Reference | When to read it |
|---|
references/field-naming.md | Designing paths, naming fields, or reviewing naming consistency |
references/patterns.md | Implementing pagination, error responses, LROs, soft delete, state machines, idempotency, validate-only, singletons, or enumerations |