When this skill is activated, always start your first response with the 🧢 emoji.
API Design
API design is the practice of defining the contract between a service and its consumers in a way that is consistent, predictable, and resilient to change. A well-designed API reduces integration friction, makes versioning safe, and communicates intent through naming and structure rather than documentation alone. This skill covers the three dominant paradigms - REST, GraphQL, and gRPC - along with OpenAPI specs, pagination strategies, versioning, error formats, and authentication patterns.
When to use this skill
Trigger this skill when the user:
- Asks how to name, structure, or version API endpoints
- Needs to choose between REST, GraphQL, or gRPC for a new service
- Wants to write or review an OpenAPI / Swagger specification
- Asks about HTTP status codes and when to use each
- Needs to implement pagination (offset, cursor, keyset)
- Asks about authentication schemes (API key, OAuth2, JWT)
- Wants a consistent error response format across their API
- Needs to design request/response schemas or query parameters
Do NOT trigger this skill for:
- Internal function/method interfaces inside a single service - use clean-code or clean-architecture skills
- Database schema design unless it is driven by API contract requirements
Key principles
- Consistency over cleverness - Every endpoint, field name, error shape, and
status code should follow the same pattern throughout the API. Consumers should be able to predict behavior for an endpoint they have never used before.
- Resource-oriented design - Model your API around nouns (resources), not
verbs (actions). POST /orders is better than POST /createOrder. The HTTP method carries the verb.
- Proper HTTP semantics - Use the right method (
GETis safe + idempotent,
PUT/DELETE are idempotent, POST is neither). Use correct status codes: 201 for creation, 204 for empty success, 400 for client errors, 404 for not found, 409 for conflicts, 429 for rate limiting.
- Version from day one - Include a version in your URL or header before
publishing. v1 in the path costs nothing; removing a breaking change from a production API costs everything.
- Design for the consumer - Shape responses around what the client needs, not
around what the database returns. Clients should not have to join, filter, or transform data after receiving a response.
Core concepts
REST resources
REST treats everything as a resource identified by a URL. Resources are manipulated through a uniform interface: GET, POST, PUT, PATCH, DELETE. Collections live at /resources and individual items at /resources/{id}. Sub-resources express ownership: /users/{id}/orders.
GraphQL schema
GraphQL exposes a single endpoint and lets clients declare exactly which fields they need. The schema is the contract - it defines types, queries, mutations, and subscriptions. Best for: UIs that need flexible data fetching, aggregating multiple back-end services, or reducing over/under-fetching.
gRPC + Protobuf
gRPC uses Protocol Buffers as its IDL and HTTP/2 as transport. It generates strongly-typed client/server stubs. Best for: internal service-to-service communication where performance, type safety, and streaming matter more than browser compatibility.
When to use which
| Need | REST | GraphQL | gRPC | |------|------|---------|------| | Public/partner API | Best | Good | Avoid | | Browser clients | Best | Best | Poor | | Internal microservices | Good | Overkill | Best | | Real-time / streaming | Polling/SSE | Subscriptions | Best | | Flexible field selection | Sparse fieldsets | Best | N/A | | Type-safe contracts | OpenAPI | Schema | Proto |
Common tasks
1. Design RESTful resource endpoints
Use lowercase, hyphen-separated plural nouns. Never use verbs in the path.
# Collections
GET /v1/articles - list
POST /v1/articles - create
# Single resource
GET /v1/articles/{id} - read
PUT /v1/articles/{id} - full replace
PATCH /v1/articles/{id} - partial update
DELETE /v1/articles/{id} - delete
# Sub-resources
GET /v1/users/{id}/orders - list orders for a user
# Actions that don't map to CRUD (use verb noun under resource)
POST /v1/orders/{id}/cancel
POST /v1/users/{id}/password-reset
2. Write an OpenAPI 3.1 spec
Always use $ref to pull components out of paths for reuse. See references/openapi-patterns.md for the full component library (security schemes, reusable responses, discriminators, webhooks).
openapi: 3.1.0
info:
title: Articles API
version: 1.0.0
servers:
- url: https://api.example.com/v1
paths:
/articles:
get:
operationId: listArticles
summary: List articles
tags: [Articles]
parameters:
- { name: cursor, in: query, schema: { type: string } }
- { name: limit, in: query, schema: { type: integer, default: 20, maximum: 100 } }
responses:
'200':
description: Paginated list of articles
content:
application/json:
schema:
$ref: '#/components/schemas/ArticleListResponse'
'400': { $ref: '#/components/responses/BadRequest' }
post:
operationId: createArticle
summary: Create an article
tags: [Articles]
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [title]
properties:
title: { type: string, maxLength: 255 }
body: { type: string }
responses:
'201':
description: Article created
content:
application/json:
schema: { $ref: '#/components/schemas/Article' }
'422': { $ref: '#/components/responses/UnprocessableEntity' }
components:
schemas:
Article:
type: object
required: [id, title, status, createdAt]
properties:
id: { type: string, format: uuid }
title: { type: string, maxLength: 255 }
status: { type: string, enum: [draft, published, archived] }
createdAt: { type: string, format: date-time }
ArticleListResponse:
type: object
required: [data, pagination]
properties:
data:
type: array
items: { $ref: '#/components/schemas/Article' }
pagination:
type: object
properties:
nextCursor: { type: [string, "null"] }
hasMore: { type: boolean }
responses:
BadRequest:
description: Invalid request
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
UnprocessableEntity:
description: Validation failed
content:
application/problem+json:
schema: { $ref: '#/components/schemas/ProblemDetails' }
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
3. Implement cursor-based pagination
Cursor pagination is stable under concurrent writes; offset pagination is not.
interface PaginationParams {
cursor?: string;
limit?: number;
}
interface PaginatedResult<T> {
data: T[];
pagination: {
nextCursor: string | null;
hasMore: boolean;
};
}
async function listArticles(
params: PaginationParams
): Promise<PaginatedResult<Article>> {
const limit = Math.min(params.limit ?? 20, 100);
// Decode opaque cursor back to an internal value
const afterId = params.cursor
? Buffer.from(params.cursor, 'base64url').toString('utf8')
: null;
const rows = await db.article.findMany({
where: afterId ? { id: { gt: afterId } } : undefined,
orderBy: { id: 'asc' },
take: limit + 1, // f