Well-designed APIs are the backbone of modern web applications. Here are essential principles and practices for creating APIs that are both powerful and developer-friendly:
✅ Good
GET /api/users
GET /api/users/123
POST /api/users
PUT /api/users/123
DELETE /api/users/123
❌ Bad
GET /api/getUsers
GET /api/user?id=123
POST /api/createUser
Use appropriate status codes:
// Success responses
200 OK // Successful GET, PUT, PATCH
201 Created // Successful POST
204 No Content // Successful DELETE
// Client error responses
400 Bad Request // Invalid request data
401 Unauthorized // Authentication required
403 Forbidden // Access denied
404 Not Found // Resource doesn't exist
409 Conflict // Resource conflict
422 Unprocessable // Validation errors
// Server error responses
500 Internal Server Error
503 Service Unavailable
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid input data",
"details": [
{
"field": "email",
"message": "Email is required"
}
],
"timestamp": "2024-11-15T10:30:00Z",
"requestId": "req_123456"
}
}
// Request
GET /api/posts?limit=20&cursor=eyJpZCI6MTIzfQ
// Response
{
"data": [...],
"pagination": {
"hasNextPage": true,
"nextCursor": "eyJpZCI6MTQ0fQ",
"limit": 20
}
}
// Complex filtering
GET /api/products?category=electronics&price[gte]=100&price[lte]=500&sort=-createdAt&limit=10
// Response includes metadata
{
"data": [...],
"meta": {
"total": 1250,
"page": 1,
"limit": 10,
"filters": {
"category": "electronics",
"price": { "gte": 100, "lte": 500 }
}
}
}
// JWT-based authentication
const authenticateToken = (req, res, next) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Invalid token' });
req.user = user;
next();
});
};
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests, please try again later'
}
});
app.use('/api', limiter);
const { body, validationResult } = require('express-validator');
const validateUser = [
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/),
body('name').trim().isLength({ min: 2, max: 50 }),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid input data',
details: errors.array()
}
});
}
next();
}
];
openapi: 3.0.0
info:
title: User API
version: 1.0.0
paths:
/users:
get:
summary: Get users
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
const redis = require('redis');
const client = redis.createClient();
const cacheMiddleware = (duration = 3600) => {
return async (req, res, next) => {
const key = `cache:${req.method}:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Store original res.json
const originalJson = res.json;
res.json = function(data) {
// Cache the response
client.setex(key, duration, JSON.stringify(data));
originalJson.call(this, data);
};
next();
} catch (error) {
next();
}
};
};
// Use database indexes
// SELECT with specific fields
const users = await db.users.findMany({
select: {
id: true,
name: true,
email: true,
// Don't select password or sensitive data
},
where: {
active: true
},
orderBy: {
createdAt: 'desc'
},
take: 20
});
// URL versioning
GET /api/v1/users
GET /api/v2/users
// Header versioning
GET /api/users
Headers: {
'Accept': 'application/vnd.api+json;version=2'
}
Following these practices will help you build APIs that are secure, performant, and maintainable.