api-design-patterns
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
More from benshapyro/cadre-devkit-claude
frontend-design
Create distinctive, memorable user interfaces that avoid generic AI aesthetics. Use when designing UI/UX, planning visual direction, or building pages and layouts.
10error-handler
Provides battle-tested error handling patterns for TypeScript and Python. Use when implementing error handling, creating try/catch blocks, or handling exceptions.
5react-patterns
Modern React patterns for TypeScript applications including hooks, state management, and component composition. Use when building React components, managing state, or implementing React best practices.
4tailwind-conventions
Consistent Tailwind CSS patterns for React/Next.js applications. Use when styling components with Tailwind, adding responsive design, implementing dark mode, or organizing utility classes.
4product-discovery
Methodology for discovering and specifying new software products. Use when starting greenfield projects, exploring new ideas, or defining MVP scope.
4test-generator
Generates Jest or Pytest tests following Ben's testing standards. Use when creating tests, adding test coverage, writing unit tests, mocking dependencies, or when user mentions testing, test cases, Jest, Pytest, fixtures, assertions, or coverage.
3