| name | api-design-patterns |
| description | Provides REST and GraphQL API design patterns for Node.js, Flask, and FastAPI. Use when designing endpoints, request/response structures, API architecture, pagination, authentication, rate limiting, or when working in /api/ or /routes/ directories. |
API Design Patterns Skill
Best practices for designing RESTful and GraphQL APIs.
Resources
For detailed code examples, see:
references/express-examples.md- Node.js + Express patternsreferences/fastapi-examples.md- FastAPI (Python) patternsreferences/flask-examples.md- Flask (Python) patterns
Core Principles
- Consistency - Use consistent naming, structure, and behavior
- RESTful - Follow REST conventions for resource-based APIs
- Versioning - Plan for API evolution
- Documentation - APIs should be self-documenting
- Error Handling - Consistent, informative error responses
- Security - Authentication, authorization, input validation
- Performance - Pagination, caching, rate limiting
URL Structure
GET /api/v1/users # List users
GET /api/v1/users/:id # Get specific user
POST /api/v1/users # Create user
PUT /api/v1/users/:id # Update user (full)
PATCH /api/v1/users/:id # Update user (partial)
DELETE /api/v1/users/:id # Delete user
GET /api/v1/users/:id/posts # Nested resource
Avoid:
- Verbs in URLs (
/getUsers) - Redundant paths (
/user/create) - Deep nesting (>2 levels)
HTTP Status Codes
| Code | Meaning | When to Use |
|---|---|---|
| 200 | OK | Success (GET, PUT, PATCH) |
| 201 | Created | Resource created (POST) |
| 204 | No Content | Success with no body (DELETE) |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Not authenticated |
| 403 | Forbidden | Authenticated but not authorized |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Duplicate or state conflict |
| 422 | Unprocessable | Validation failed |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Error | Server error |
Response Format
Success Response
{
"data": {
"id": "123",
"name": "John Doe"
},
"meta": {
"timestamp": "2025-10-26T10:00:00Z"
}
}
List Response (Paginated)
{
"data": [
{ "id": "1", "name": "User 1" }
],
"meta": {
"total": 100,
"page": 1,
"perPage": 20,
"totalPages": 5
},
"links": {
"first": "/api/v1/users?page=1",
"prev": null,
"next": "/api/v1/users?page=2",
"last": "/api/v1/users?page=5"
}
}
Error Response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
}
}
API Versioning
// URL versioning (recommended)
app.use('/api/v1', routesV1);
app.use('/api/v2', routesV2);
// Deprecation headers
res.setHeader('Deprecation', 'true');
res.setHeader('Sunset', 'Wed, 11 Nov 2025 11:11:11 GMT');
GraphQL Patterns
Schema Design
type User {
id: ID!
name: String!
email: String!
posts: [Post!]! # Resolver handles N+1 with DataLoader
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User!
}
type Query {
user(id: ID!): User
users(first: Int, after: String): UserConnection!
}
type Mutation {
createUser(input: CreateUserInput!): User!
updateUser(id: ID!, input: UpdateUserInput!): User!
deleteUser(id: ID!): Boolean!
}
input CreateUserInput {
name: String!
email: String!
}
Resolver Patterns (Node.js)
const resolvers = {
Query: {
user: async (_, { id }, { dataSources }) => {
return dataSources.usersAPI.getUser(id);
},
users: async (_, { first, after }, { dataSources }) => {
return dataSources.usersAPI.getUsers({ first, after });
},
},
Mutation: {
createUser: async (_, { input }, { dataSources }) => {
return dataSources.usersAPI.createUser(input);
},
},
User: {
// Field resolver with DataLoader to prevent N+1
posts: async (user, _, { loaders }) => {
return loaders.postsByUserId.load(user.id);
},
},
};
Error Handling
// Throw typed errors
import { GraphQLError } from 'graphql';
throw new GraphQLError('User not found', {
extensions: {
code: 'NOT_FOUND',
http: { status: 404 },
},
});
// Common error codes
// UNAUTHENTICATED, FORBIDDEN, NOT_FOUND, VALIDATION_ERROR, INTERNAL_ERROR
Pagination (Relay Cursor-Based)
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
GraphQL vs REST - When to Use
| Use GraphQL | Use REST |
|---|---|
| Multiple clients with different data needs | Simple CRUD operations |
| Deeply nested data in single request | Caching critical (HTTP caching) |
| Rapid iteration, evolving schema | Public API with stability guarantees |
| Mobile apps (minimize requests) | File uploads, streaming |
Quick Reference
Do
- Use plural nouns for collections (
/users) - Return appropriate status codes
- Validate all inputs
- Implement pagination for lists
- Use consistent error format
- Version your APIs
- Document with OpenAPI/Swagger
- Implement rate limiting
- Use HTTPS in production
Don't
- Use verbs in URLs
- Nest resources more than 2 levels
- Return sensitive data
- Use HTTP 200 for errors
- Expose internal error details
- Skip input validation
- Return huge lists without pagination
Input Validation
Always validate using schemas:
TypeScript (Zod):
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
});
Python (Pydantic):
class UserCreate(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
age: Optional[int] = Field(None, ge=0, le=150)
Authentication Pattern
- Extract token from
Authorization: Bearer <token>header - Verify token signature and expiration
- Load user from token payload
- Attach user to request context
- Return 401 for invalid/missing tokens
Authorization Pattern
- Check user role/permissions after authentication
- Use middleware/decorators for role checks
- Return 403 for insufficient permissions
Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // requests per window
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests',
},
},
});
Testing APIs
describe('GET /api/v1/users', () => {
it('returns paginated users', async () => {
const response = await request(app)
.get('/api/v1/users?page=1')
.expect(200);
expect(response.body).toHaveProperty('data');
expect(response.body).toHaveProperty('meta');
});
it('returns 401 without auth', async () => {
await request(app).get('/api/v1/users').expect(401);
});
});
Good API design makes your API intuitive, consistent, and easy to use.
Version
- v1.1.0 (2025-12-05): Enriched trigger keywords in description
- v1.0.0 (2025-11-15): Initial version