api-design-expert
Design professional REST, GraphQL, and gRPC APIs. Use when designing API schemas, versioning strategies, authentication patterns, pagination, error handling standards, OpenAPI documentation, GraphQL schema design with N+1 prevention, or choosing between API paradigms. Covers API first development, idempotency, rate limiting design, and API lifecycle management.
API Design Expert
REST API Design Principles
Resource Naming
✅ Correct:
GET /users → List users
POST /users → Create user
GET /users/{id} → Get user
PUT /users/{id} → Replace user (full update)
PATCH /users/{id} → Update user (partial)
DELETE /users/{id} → Delete user
GET /users/{id}/orders → User's orders
POST /users/{id}/orders → Create order for user
❌ Wrong:
GET /getUser
POST /createUser
GET /user/getAll
POST /users/delete/{id} → Use DELETE verb
HTTP Status Codes
2xx Success:
200 OK → GET, PUT, PATCH success
201 Created → POST success; include Location header
202 Accepted → Async operation started (not yet complete)
204 No Content → DELETE, or PUT/PATCH that returns nothing
3xx Redirect:
301 Moved Permanently → Old URL gone forever
302 Found → Temporary redirect
4xx Client Error:
400 Bad Request → Malformed request, validation error
401 Unauthorized → Not authenticated (no/invalid token)
403 Forbidden → Authenticated but not authorized
404 Not Found → Resource doesn't exist
409 Conflict → Duplicate, optimistic locking failure
410 Gone → Resource permanently deleted
422 Unprocessable → Validation failed (semantic error)
429 Too Many Requests → Rate limit exceeded
5xx Server Error:
500 Internal Server Error → Bug / unhandled error
502 Bad Gateway → Upstream service failed
503 Service Unavailable → Overloaded or maintenance
504 Gateway Timeout → Upstream timeout
Consistent Error Response
{
"error": {
"code": "VALIDATION_FAILED",
"message": "Request validation failed",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "Must be a valid email address"
},
{
"field": "age",
"code": "OUT_OF_RANGE",
"message": "Must be between 18 and 120"
}
],
"request_id": "req_8f7d6e5c4b3a",
"docs_url": "https://api.example.com/docs/errors#VALIDATION_FAILED"
}
}
Pagination
Offset pagination:
GET /users?limit=20&offset=0
GET /users?limit=20&offset=20
✅ Simple, random access
❌ Performance degrades at high offset
❌ Inconsistent results when data changes during pagination
Cursor pagination (preferred for large datasets):
GET /users?limit=20
Response: { "data": [...], "cursor": "eyJpZCI6MTAwfQ==" }
GET /users?limit=20&cursor=eyJpZCI6MTAwfQ==
✅ Consistent results (no skipping/duplicates)
✅ O(log n) performance with index
❌ No random access ("go to page 5")
Page-based (user-friendly):
GET /users?page=1&per_page=20
Response: { "data": [...], "pagination": { "page": 1, "per_page": 20, "total": 150, "total_pages": 8 } }
Standard pagination envelope:
{
"data": [...],
"pagination": {
"cursor": "next_cursor_value", // or "next_page"
"has_more": true,
"total": 1500 // optional, expensive to compute
}
}
Versioning Strategies
1. URL versioning (most common)
/api/v1/users
/api/v2/users
✅ Explicit, cacheable
❌ URL pollution
2. Header versioning
Accept: application/vnd.myapi.v2+json
API-Version: 2024-01
✅ Clean URLs
❌ Less visible, harder to test in browser
3. Query parameter
GET /users?version=2
✅ Easy to test
❌ Easy to forget, breaks caching
Recommendation: URL versioning + Sunset header for deprecation
API-Deprecation: Fri, 01 Jan 2026 00:00:00 GMT
Sunset: Fri, 01 Jan 2026 00:00:00 GMT
Link: <https://api.example.com/v2>; rel="successor-version"
Authentication Patterns
API Key (simple services, M2M):
Authorization: Bearer sk_live_abc123
X-API-Key: sk_live_abc123
Store: hash (bcrypt/SHA-256), never plaintext
Prefix with env: sk_live_ sk_test_ sk_dev_
JWT (stateless, user sessions):
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
Best practices:
- Short expiry: 15-60 min for access tokens
- Refresh token: 7-30 days, stored in httpOnly cookie
- Use RS256 (asymmetric) for microservices
- Include: sub, iat, exp, jti (JWT ID for revocation)
OAuth 2.1 (third-party access):
- Authorization Code + PKCE (browser/mobile)
- Client Credentials (M2M)
- Never use Implicit flow (deprecated)
mTLS (high-security M2M):
Both parties present certificates
Best for: financial services, healthcare
Idempotency
Idempotency key pattern — safe retries for non-GET operations:
POST /payments
Idempotency-Key: a8098c1a-f86e-11da-bd1a-00112444be1e
Server behavior:
1. Check if key seen before
2. If yes: return cached response (same status code + body)
3. If no: process request, cache response for 24 hours, respond
// Express middleware
async function idempotencyMiddleware(req, res, next) {
const key = req.headers['idempotency-key'];
if (!key || req.method === 'GET') return next();
const cached = await redis.get(`idempotency:${key}`);
if (cached) {
const { status, body } = JSON.parse(cached);
return res.status(status).json(body);
}
// Intercept response to cache it
const originalJson = res.json.bind(res);
res.json = (body) => {
redis.setex(`idempotency:${key}`, 86400, JSON.stringify({
status: res.statusCode,
body,
}));
return originalJson(body);
};
next();
}
OpenAPI/Swagger Documentation
# openapi.yaml
openapi: "3.1.0"
info:
title: Users API
version: "2.0.0"
contact:
email: [email protected]
license:
name: Apache 2.0
servers:
- url: https://api.example.com/v2
description: Production
- url: https://sandbox.example.com/v2
description: Sandbox
paths:
/users:
get:
summary: List users
operationId: listUsers
tags: [Users]
parameters:
- name: cursor
in: query
schema:
type: string
description: Pagination cursor from previous response
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
"200":
description: Paginated user list
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
"401":
$ref: '#/components/responses/Unauthorized'
"429":
$ref: '#/components/responses/RateLimited'
security:
- bearerAuth: []
post:
summary: Create user
operationId: createUser
tags: [Users]
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
"201":
description: User created
headers:
Location:
schema:
type: string
description: URL of created user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
"422":
$ref: '#/components/responses/ValidationError'
components:
schemas:
User:
type: object
required: [id, email, created_at]
properties:
id:
type: string
format: uuid
readOnly: true
email:
type: string
format: email
name:
type: string
maxLength: 100
created_at:
type: string
format: date-time
readOnly: true
CreateUserRequest:
type: object
required: [email, name]
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
responses:
Unauthorized:
description: Authentication required
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
RateLimited:
description: Rate limit exceeded
headers:
Retry-After:
schema:
type: integer
description: Seconds until rate limit resets
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
GraphQL Schema Design
# schema.graphql — Good schema design
type Query {
user(id: ID!): User
users(
first: Int
after: String # Cursor-based pagination (Relay spec)
filter: UserFilter
orderBy: UserOrderBy
): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): CreateUserPayload!
updateUser(id: ID!, input: UpdateUserInput!): UpdateUserPayload!
}
type Subscription {
userCreated: User!
orderStatusChanged(orderId: ID!): Order!
}
# Relay-spec connection (enables pagination)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# Payload types for mutations (best practice)
type CreateUserPayload {
user: User
errors: [UserError!]!
}
type UserError {
field: String
code: String!
message: String!
}
type User {
id: ID!
email: String!
name: String!
createdAt: DateTime!
# Nested connections (N+1 risk — use DataLoader!)
orders(first: Int, after: String): OrderConnection!
posts(first: Int): PostConnection!
}
# Input types
input CreateUserInput {
email: String!
name: String!
role: UserRole = USER
}
enum UserRole {
ADMIN
USER
GUEST
}
scalar DateTime
scalar UUID
N+1 Prevention with DataLoader
// Without DataLoader: 1 query for users + N queries for each user's orders
// With DataLoader: 1 query for users + 1 batched query for ALL orders
import DataLoader from 'dataloader';
// Create per-request dataloader (not global — prevents cache bleed)
function createLoaders(db: DB) {
return {
orders: new DataLoader<string, Order[]>(async (userIds) => {
const orders = await db.query(
'SELECT * FROM orders WHERE user_id = ANY($1)',
[userIds as string[]]
);
// Group by userId and preserve order
return userIds.map(id => orders.filter(o => o.userId === id));
}),
user: new DataLoader<string, User | null>(async (ids) => {
const users = await db.query(
'SELECT * FROM users WHERE id = ANY($1)',
[ids as string[]]
);
const map = new Map(users.map(u => [u.id, u]));
return ids.map(id => map.get(id) ?? null);
}),
};
}
// In resolver
const resolvers = {
User: {
orders: async (user, args, context) => {
return context.loaders.orders.load(user.id); // Batched automatically
}
}
};
gRPC Design
// users.proto
syntax = "proto3";
package users.v1;
import "google/protobuf/timestamp.proto";
service UserService {
// Unary RPC
rpc GetUser(GetUserRequest) returns (User);
rpc CreateUser(CreateUserRequest) returns (User);
// Server streaming — good for real-time feeds
rpc ListUsers(ListUsersRequest) returns (stream User);
// Bidirectional streaming — good for chat
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest {
string user_id = 1;
}
message User {
string id = 1;
string email = 2;
string name = 3;
google.protobuf.Timestamp created_at = 4;
UserRole role = 5;
}
enum UserRole {
USER_ROLE_UNSPECIFIED = 0; // Always start with 0 as unknown
USER_ROLE_ADMIN = 1;
USER_ROLE_USER = 2;
}
message CreateUserRequest {
string email = 1;
string name = 2;
}
API Design Checklist
Design:
[ ] Resources are nouns, not verbs
[ ] Consistent naming convention (snake_case vs camelCase — pick one)
[ ] Appropriate HTTP verbs used
[ ] Status codes are correct
[ ] Error responses are consistent and include error codes
Security:
[ ] Authentication required (except explicitly public endpoints)
[ ] Authorization checked (user can only see their own data)
[ ] Input validation on all fields
[ ] Rate limiting implemented
[ ] Idempotency keys for mutations
Documentation:
[ ] OpenAPI/GraphQL schema is current
[ ] Error codes are documented
[ ] Rate limits documented
[ ] Authentication flow documented
[ ] Example requests/responses provided
Reliability:
[ ] Pagination on all list endpoints
[ ] Timeouts set appropriately
[ ] Deprecated endpoints have Sunset headers
[ ] Breaking changes use new versionSkill Information
- Source
- MoltbotDen
- Category
- Coding Agents & IDEs
- Repository
- View on GitHub
Related Skills
go-expert
Write idiomatic, production-quality Go code. Use when building Go APIs, CLIs, microservices, or systems code. Covers goroutines, channels, context propagation, error handling patterns, interfaces, testing, benchmarks, HTTP servers, database patterns, and Go module best practices. Expert-level Go idioms that senior engineers expect.
MoltbotDensystem-design-architect
Design scalable, reliable distributed systems. Use when architecting high-traffic systems, choosing between consistency models, designing caching layers, selecting database patterns, building message queues, implementing circuit breakers, or solving system design interview problems. Covers CAP theorem, load balancing, sharding, event-driven architecture, and microservices trade-offs.
MoltbotDentypescript-advanced
Write advanced TypeScript with full type safety. Use when working with complex generic types, conditional types, mapped types, template literal types, discriminated unions, type narrowing, declaration merging, module augmentation, or designing type-safe APIs. Covers TypeScript 5.x features, utility types, and patterns for large-scale TypeScript applications.
MoltbotDenrust-systems
Write safe, performant Rust systems code. Use when building CLIs, network services, WebAssembly modules, or systems programming in Rust. Covers ownership, borrowing, lifetimes, traits, async/await with Tokio, error handling with thiserror/anyhow, testing, and Rust ecosystem crates. Idiomatic Rust patterns that pass code review.
MoltbotDengraphql-expert
Design and implement production GraphQL APIs. Use when designing GraphQL schemas, implementing resolvers, solving N+1 problems with DataLoader, implementing subscriptions, building GraphQL federation, generating types from schemas, or optimizing GraphQL performance. Covers Apollo Server, GraphQL Yoga, Pothos schema builder, and persisted queries.
MoltbotDen