About api-design
A protocol-level reference for people designing or reviewing APIs, applicable across languages and frameworks. It covers REST and GraphQL choices, error handling, versioning, pagination, and authentication, with practical guidance and a quality checklist.
Key features
- 01REST and GraphQL selection guidance
- 02Resource, HTTP, and schema principles
- 03Error formats and versioning guidance
- 04Pagination and authentication advice
- 05Antipatterns and quality checklist
Use cases
- 01Planning an API's protocol and interface
- 02Reviewing an API for consistency
- 03Defining error and versioning strategies
- 04Establishing API design standards for a team
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ krzysztofsurdy/ code-virtuoso --skill api-designAfter installing, type/api-designin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
references/api-evolution.mdgraphql-patterns.mdrest-patterns.mdDetails
From the project READMEAPI Design
Principles and patterns for designing APIs that are consistent, predictable, and easy to evolve. Applies to any language or framework. The focus is protocol-level design decisions, not implementation details.
Treat the API surface as a product: consumers should be able to predict behavior, recover from errors, and integrate without reading source code.
When to use
- Designing a new public or internal API from scratch
- Reviewing an existing API for consistency and usability
- Choosing between REST and GraphQL for a project
- Planning API versioning or migration strategy
- Defining error response contracts across services
- Establishing API standards for a team or organization
REST vs GraphQL
| Aspect | REST | GraphQL |
|---|---|---|
| Best for | CRUD-heavy, resource-oriented domains | Complex, interconnected data with varied client needs |
| Data fetching | Fixed response shapes per endpoint | Client specifies exact fields needed |
| Over-fetching | Common, endpoints return full resources | Eliminated, clients request only what they need |
| Under-fetching | Common, requires multiple round trips | Eliminated, a single query can span relations |
| Caching | Built-in HTTP caching (ETags, Cache-Control) | Requires custom caching (normalized stores, persisted queries) |
| File uploads | Native multipart support | Requires workarounds (multipart spec or separate endpoint) |
| Real-time | Webhooks, SSE, or polling | Subscriptions built into the spec |
| Tooling maturity | Mature: OpenAPI, Postman, HTTP clients | Growing: Apollo, Relay, GraphiQL |
| Learning curve | Lower, builds on existing HTTP knowledge | Higher: schema language, resolvers, query optimization |
| Error handling | HTTP status codes + response body | Executed operations usually return 200 with errors in the response errors array; request errors can use 4xx (GraphQL over HTTP, still a draft) |
| Versioning | URL path, headers, or query params | Schema evolution via deprecation + additive changes |
Choose REST when: your domain maps to resources and CRUD operations, you need HTTP caching, or your clients are simple (mobile apps, third-party integrations).
Choose GraphQL when: clients have varied data needs, you are aggregating multiple backend services, or you want a strongly typed contract between frontend and backend.
Both are valid. Many systems use REST for external/public APIs and GraphQL for internal frontend-backend communication.
REST design principles
REST APIs model the domain as resources and use HTTP semantics to operate on them.
Core rules:
- Resources are nouns, not verbs:
/orders, not/getOrders - HTTP methods are the verbs: GET reads, POST creates, PUT replaces, PATCH updates, DELETE removes
- URLs identify resources; query parameters filter, sort, or paginate them
- Use plural nouns for collections:
/users,/users/{id} - Limit nesting to two levels:
/users/{id}/ordersis fine;/users/{id}/orders/{id}/items/{id}/variantsis not - Use HTTP status codes meaningfully. Do not return 200 for everything
- Support content negotiation via
AcceptandContent-Typeheaders
HATEOAS (Hypermedia as the Engine of Application State) adds discoverability by including links in responses. It is useful for public APIs but often overkill for internal services:
{
"id": 42,
"status": "shipped",
"_links": {
"self": { "href": "/orders/42" },
"cancel": { "href": "/orders/42/cancel", "method": "POST" },
"customer": { "href": "/customers/7" }
}
}
See REST Patterns Reference for detailed conventions.
GraphQL design principles
GraphQL APIs expose a strongly typed schema that clients query declaratively.
Core rules:
- Design schema-first: define the type system before writing resolvers
- Types represent domain concepts; fields represent attributes and relations
- Queries read data, mutations write data, subscriptions stream data
- Use the type system to enforce constraints (non-null, enums, input types)
- Avoid deeply nested schemas that create unpredictable query costs
- Solve N+1 problems with batching (dataloader pattern)
- Limit query depth and complexity to prevent abuse
Schema-first example:
type User {
id: ID!
name: String!
email: String!
orders(first: Int, after: String): OrderConnection!
}
type Order {
id: ID!
total: Float!
status: OrderStatus!
createdAt: DateTime!
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
See GraphQL Patterns Reference for detailed conventions.
Error handling
A consistent error format is one of the highest-leverage API design decisions. Consumers should be able to parse errors programmatically without inspecting message strings.
RFC 9457 Problem Details format (recommended for REST; it obsoletes RFC 7807):
Send it with Content-Type: application/problem+json. Members beyond the standard five (type, title, status, detail, instance) are extension members, such as errors below.
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "Insufficient Funds",
"status": 422,
"detail": "Account balance is $10.00 but the transfer requires $50.00.",
"instance": "/transfers/abc-123",
"errors": [
{
"field": "amount",
"code": "insufficient_funds",
"message": "Transfer amount exceeds available balance"
}
]
}
Key principles:
- Use a machine-readable
typeorcode. Clients should branch on codes, not messages - Include a human-readable
detailfor debugging - Give each error kind a stable, documented
typeURI. Omittype(or useabout:blank) only when the HTTP status says everything - Return field-level errors for validation failures so clients can highlight specific inputs
- Use appropriate HTTP status codes (REST) or structured error types (GraphQL)
- Never expose stack traces, internal paths, or SQL queries in production
- Include a correlation/request ID for tracing errors across services
GraphQL error conventions:
{
"data": { "createOrder": null },
"errors": [
{
"message": "Insufficient funds",
"extensions": {
"code": "INSUFFICIENT_FUNDS",
"field": "amount"
}
}
]
}
Versioning
The versioning strategy decides how you ship changes without breaking existing consumers.
| Strategy | Mechanism | Pros | Cons |
|---|---|---|---|
| URL path | /v1/users | Explicit, easy to route | URL pollution, hard to deprecate |
| Accept header | Accept: application/vnd.api+json;version=2 | Clean URLs, HTTP-correct | Less visible, harder to test casually |
| Query param | /users?version=2 | Simple to implement | Easy to forget, caching complications |
Practical guidance:
- URL path versioning is the most common and easiest for consumers to understand
- Only bump the major version for breaking changes
- Prefer evolving the API additively (new fields, new endpoints) over creating new versions
- When a version is deprecated, communicate a sunset date and provide migration guides
See API Evolution Reference for detailed strategies.
Pagination
Every list endpoint needs pagination. Cursor versus offset affects performance, consistency, and client complexity.
| Approach | How it works | Pros | Cons |
|---|---|---|---|
| Offset | ?offset=20&limit=10 | Simple, supports "jump to page N" | Inconsistent with real-time inserts/deletes, slow on large tables |
| Cursor | ?after=abc123&limit=10 | Stable with real-time data, performant at scale | Cannot jump to arbitrary pages |
Best practices:
- Set a maximum page size (e.g., 100) and a sensible default (e.g., 20)
- Return pagination metadata:
hasNextPage,hasPreviousPage,totalCount(if cheap to compute) - If
totalCountis expensive, make it optional or return an estimate - Use cursors for feeds, activity streams, and any data that changes frequently
- Use offset for admin dashboards, reports, and datasets that rarely change during browsing
Cursor pagination response example:
{
"data": [ ... ],
"pagination": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "eyJpZCI6MX0=",
"endCursor": "eyJpZCI6MTB9"
}
}
Authentication and authorization
Authentication verifies identity (who are you?). Authorization verifies permissions (what can you do?).
| Mechanism | Use case | Notes |
|---|---|---|
| API keys | Server-to-server, simple integrations | Easy to implement; rotate regularly; never expose in client code |
| OAuth 2.0 | Third-party access, delegated permissions | Industry standard. Use Authorization Code + PKCE for SPAs/mobile |
| JWT (Bearer tokens) | Stateless auth for microservices | Include only essential claims; set short expiry; validate signature and claims |
| Session cookies | Browser-based web apps | Pair with CSRF protection; use Secure, HttpOnly, SameSite flags |
Best practices:
- Always use HTTPS, no exceptions
- Transmit tokens in
Authorization: Bearer <token>header, not in query strings - Implement scopes/permissions for fine-grained access control
- Return
401 Unauthorizedfor missing/invalid credentials,403 Forbiddenfor insufficient permissions - Rate-limit authentication endpoints aggressively to prevent brute-force attacks
- Support token refresh flows to avoid forcing re-authentication
Common antipatterns
| Antipattern | Problem | Fix |
|---|---|---|
| Chatty API | Clients need 10+ requests to render a page | Aggregate related data; consider GraphQL or composite endpoints |
| God endpoint | Single endpoint accepts wildly different payloads via flags | Split into focused endpoints with clear semantics |
| Inconsistent naming | Mix of snake_case, camelCase, plural/singular | Pick one convention and enforce it project-wide |
| Missing pagination | List endpoints return unbounded results | Always paginate collections; set max page size |
| Breaking changes without versioning | Renaming or removing fields breaks clients silently | Use versioning or additive-only evolution |
| Leaking internals | Database column names, auto-increment IDs in URLs | Map to stable external identifiers (UUIDs, slugs) |
| Ignoring idempotency | Retrying a POST creates duplicate resources | Support idempotency keys for non-idempotent operations |
| 200 for everything | Errors return HTTP 200 with an error body | Use appropriate HTTP status codes |
| Timestamps without timezone | 2024-01-15 14:30:00 is ambiguous | Always use ISO 8601 with timezone: 2024-01-15T14:30:00Z |
Quality checklist
Before shipping or reviewing an API, check that:
- Resource naming is consistent (plural nouns, no verbs in URLs)
- HTTP methods match semantics (GET is safe, PUT/DELETE are idempotent)
- Every list endpoint is paginated with a max page size
- Error responses use a consistent format with machine-readable codes
- Authentication is required and uses HTTPS
- Rate limiting is in place with appropriate headers (
Retry-After, plusRateLimitfields or legacyX-RateLimit-*) - Breaking changes are versioned or avoided via additive evolution
- Request/response examples exist for every endpoint
- Timestamps use ISO 8601 with timezone
- IDs are stable external identifiers, not internal auto-increments
- CORS is configured for browser clients (if applicable)
- Compression (gzip/brotli) is enabled for responses
- API documentation is generated from the source of truth (OpenAPI schema, GraphQL introspection)