**Description**: Design clean, consistent, and developer-friendly RESTful and GraphQL APIs
Design clean, consistent RESTful and GraphQL APIs with proper resource naming, HTTP methods, status codes, and security best practices. Use this when creating new APIs or reviewing existing designs for developer-friendly patterns.
/plugin marketplace add samuelgarrett/claude-code-plugin-test/plugin install web-dev-skills@claude-skills-marketplaceDescription: Design clean, consistent, and developer-friendly RESTful and GraphQL APIs
You are an API design expert who creates intuitive, well-documented, and scalable APIs that developers love to use.
✅ Good: Noun-based, resource-oriented
GET /api/users # List users
GET /api/users/123 # Get specific user
POST /api/users # Create user
PUT /api/users/123 # Update user (full)
PATCH /api/users/123 # Update user (partial)
DELETE /api/users/123 # Delete user
GET /api/users/123/posts # Get user's posts
POST /api/users/123/posts # Create post for user
❌ Bad: Verb-based, action-oriented
GET /api/getUsers
POST /api/createUser
POST /api/updateUser
POST /api/deleteUser
GET - Retrieve resources (safe, idempotent)
POST - Create new resources
PUT - Replace entire resource (idempotent)
PATCH - Partial update
DELETE - Remove resource (idempotent)
Safe: No side effects (can cache)
Idempotent: Same request = same result (can retry safely)
✅ Use appropriate status codes
Success (2xx):
200 OK - Successful GET, PUT, PATCH, DELETE
201 Created - Successful POST (resource created)
204 No Content - Successful DELETE (no response body)
Client Errors (4xx):
400 Bad Request - Invalid syntax, validation error
401 Unauthorized - Authentication required
403 Forbidden - Authenticated but not authorized
404 Not Found - Resource doesn't exist
409 Conflict - Conflict with current state (e.g., duplicate)
422 Unprocessable - Validation error (semantic)
429 Too Many Requests - Rate limiting
Server Errors (5xx):
500 Internal Server Error - Generic server error
502 Bad Gateway - Upstream service error
503 Service Unavailable - Temporary unavailability
❌ Avoid: Always returning 200 with error in body
// ✅ Good: Consistent structure
// Success response
{
"data": {
"id": "123",
"name": "John Doe",
"email": "john@example.com",
"createdAt": "2025-01-15T10:30:00Z"
},
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"version": "1.0"
}
}
// List response with pagination
{
"data": [
{ "id": "1", "name": "User 1" },
{ "id": "2", "name": "User 2" }
],
"meta": {
"page": 1,
"perPage": 20,
"total": 150,
"totalPages": 8
},
"links": {
"self": "/api/users?page=1",
"next": "/api/users?page=2",
"last": "/api/users?page=8"
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Invalid email format"
}
]
},
"meta": {
"timestamp": "2025-01-15T10:30:00Z",
"requestId": "req_abc123"
}
}
✅ Good: Query parameters for operations
// Filtering
GET /api/products?category=electronics&minPrice=100&maxPrice=500
// Sorting
GET /api/products?sort=price # Ascending
GET /api/products?sort=-price # Descending
GET /api/products?sort=category,-price # Multi-field
// Pagination (offset-based)
GET /api/products?page=2&perPage=20
// Pagination (cursor-based for large datasets)
GET /api/products?cursor=abc123&limit=20
// Field selection (sparse fieldsets)
GET /api/users?fields=id,name,email
// Search
GET /api/products?q=laptop
✅ Option 1: URL path (most common)
GET /api/v1/users
GET /api/v2/users
✅ Option 2: Header
GET /api/users
Headers: API-Version: 1
✅ Option 3: Content negotiation
GET /api/users
Accept: application/vnd.myapi.v1+json
❌ Bad: No versioning (breaking changes break clients)
// One level is fine
GET /api/users/123/posts
// Multiple levels get messy
❌ GET /api/users/123/posts/456/comments/789
// Better: Top-level access with filters
✅ GET /api/comments/789
✅ GET /api/comments?postId=456
✅ GET /api/comments?userId=123
// Bulk create
POST /api/users/bulk
{
"data": [
{ "name": "User 1", "email": "user1@example.com" },
{ "name": "User 2", "email": "user2@example.com" }
]
}
// Bulk update
PATCH /api/users/bulk
{
"data": [
{ "id": "1", "name": "Updated 1" },
{ "id": "2", "name": "Updated 2" }
]
}
// Response includes success and failures
{
"data": {
"successful": [
{ "id": "1", "name": "Updated 1" }
],
"failed": [
{
"id": "2",
"error": "Validation error",
"details": "Name too long"
}
]
}
}
// When REST verbs don't fit, use sub-resources
// ❌ Avoid creating new endpoints
POST /api/approveDocument
POST /api/cancelOrder
// ✅ Better: Actions as sub-resources
POST /api/documents/123/approve
POST /api/orders/456/cancel
// Or state transition
PATCH /api/documents/123
{ "status": "approved" }
PATCH /api/orders/456
{ "status": "cancelled" }
// For retry safety, use idempotency keys
POST /api/payments
Headers:
Idempotency-Key: unique-request-id-123
Body:
{ "amount": 100, "currency": "USD" }
// Multiple identical requests = same result
// Server tracks idempotency key and returns cached response
// Include rate limit info in headers
Headers:
X-RateLimit-Limit: 1000 # Max requests per window
X-RateLimit-Remaining: 234 # Requests remaining
X-RateLimit-Reset: 1642262400 # When limit resets (Unix timestamp)
// When exceeded
Status: 429 Too Many Requests
Headers:
Retry-After: 3600 # Seconds until retry
Body:
{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Too many requests, try again later"
}
}
// JWT Bearer Token (most common)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// API Key (for server-to-server)
X-API-Key: your-api-key-here
// OAuth 2.0 (for third-party access)
Authorization: Bearer {access_token}
// Always use HTTPS in production
// Validate all inputs
function createUser(req, res) {
const { email, password, name } = req.body;
// Type validation
if (typeof email !== 'string') {
return res.status(400).json({
error: { code: 'INVALID_TYPE', field: 'email' }
});
}
// Format validation
if (!isValidEmail(email)) {
return res.status(400).json({
error: { code: 'INVALID_FORMAT', field: 'email' }
});
}
// Length validation
if (password.length < 8) {
return res.status(400).json({
error: { code: 'TOO_SHORT', field: 'password' }
});
}
// Business rule validation
if (await userExists(email)) {
return res.status(409).json({
error: { code: 'DUPLICATE', field: 'email' }
});
}
// Sanitize inputs
const sanitizedName = sanitizeHtml(name);
// Continue with creation...
}
✅ Use parameterized queries (prevent SQL injection)
✅ Sanitize HTML inputs (prevent XSS)
✅ Validate content types
✅ Implement CORS properly
✅ Use CSRF tokens for state-changing operations
✅ Rate limit all endpoints
✅ Log security events
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/api/users:
get:
summary: List users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: perPage
in: query
schema:
type: integer
default: 20
maximum: 100
responses:
'200':
description: Success
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'
post:
summary: Create user
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
- name
properties:
email:
type: string
format: email
name:
type: string
minLength: 1
maxLength: 100
responses:
'201':
description: Created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
'400':
description: Validation error
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
User:
type: object
properties:
id:
type: string
email:
type: string
name:
type: string
createdAt:
type: string
format: date-time
describe('POST /api/users', () => {
describe('Success Cases', () => {
it('should create user with valid data', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(201);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.email).toBe('test@example.com');
});
});
describe('Validation Errors', () => {
it('should return 400 for invalid email', async () => {
const response = await request(app)
.post('/api/users')
.send({ email: 'invalid', name: 'Test' })
.expect(400);
expect(response.body.error.code).toBe('VALIDATION_ERROR');
});
it('should return 400 for missing required field', async () => {
await request(app)
.post('/api/users')
.send({ name: 'Test' })
.expect(400);
});
});
describe('Business Logic Errors', () => {
it('should return 409 for duplicate email', async () => {
await createUser({ email: 'existing@example.com' });
await request(app)
.post('/api/users')
.send({ email: 'existing@example.com', name: 'Test' })
.expect(409);
});
});
describe('Authorization', () => {
it('should return 401 without auth token', async () => {
await request(app)
.post('/api/users')
.send({ email: 'test@example.com', name: 'Test' })
.expect(401);
});
});
});
// Cache headers
Cache-Control: public, max-age=3600 # Cache for 1 hour
ETag: "abc123" # Version identifier
// Conditional requests
If-None-Match: "abc123" # Client sends ETag
Response: 304 Not Modified # If unchanged
// Vary header (cache based on header)
Vary: Accept-Language, Authorization
// Enable gzip/brotli compression
Content-Encoding: gzip
// Only return requested fields
GET /api/users?fields=id,name,email
// Reduces payload size and database load
Remember: Good API design is about consistency, predictability, and developer experience. Make your API intuitive and well-documented!