REST API Design: Building Intuitive and Scalable APIs

REST API Design: Building Intuitive and Scalable APIs
Well-designed REST APIs are intuitive, consistent, and scalable. Let’s explore the principles and patterns that make APIs developer-friendly and maintainable.
Resource-Based URL Structure
Design URLs around resources, not actions:
// Bad - Action-based URLs
POST /getAllUsers
POST /createNewUser
POST /updateUser
POST /deleteUser
// Good - Resource-based URLs
GET /users // Get all users
POST /users // Create a user
GET /users/:id // Get a specific user
PUT /users/:id // Update a user
PATCH /users/:id // Partially update a user
DELETE /users/:id // Delete a userHTTP Methods (Verbs)
Use appropriate HTTP methods for operations:
const express = require('express');
const router = express.Router();
// GET - Retrieve resources (safe, idempotent)
router.get('/articles', async (req, res) => {
try {
const articles = await Article.find();
res.json({
success: true,
data: articles
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// POST - Create a new resource (not idempotent)
router.post('/articles', async (req, res) => {
try {
const article = new Article(req.body);
await article.save();
res.status(201).json({
success: true,
data: article
});
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// PUT - Replace entire resource (idempotent)
router.put('/articles/:id', async (req, res) => {
try {
const article = await Article.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, overwrite: true }
);
res.json({
success: true,
data: article
});
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// PATCH - Partially update resource (idempotent)
router.patch('/articles/:id', async (req, res) => {
try {
const article = await Article.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
res.json({
success: true,
data: article
});
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});
// DELETE - Remove resource (idempotent)
router.delete('/articles/:id', async (req, res) => {
try {
await Article.findByIdAndDelete(req.params.id);
res.status(204).send();
} catch (error) {
res.status(400).json({ success: false, message: error.message });
}
});HTTP Status Codes
Use appropriate status codes:
// Success codes
const SUCCESS_CODES = {
OK: 200, // GET success
CREATED: 201, // POST success (resource created)
NO_CONTENT: 204, // DELETE success (no content to return)
};
// Client error codes
const CLIENT_ERROR_CODES = {
BAD_REQUEST: 400, // Invalid request data
UNAUTHORIZED: 401, // Authentication required
FORBIDDEN: 403, // Authenticated but not authorized
NOT_FOUND: 404, // Resource not found
CONFLICT: 409, // Conflict (e.g., duplicate email)
UNPROCESSABLE_ENTITY: 422, // Validation errors
TOO_MANY_REQUESTS: 429, // Rate limit exceeded
};
// Server error codes
const SERVER_ERROR_CODES = {
INTERNAL_SERVER_ERROR: 500, // Generic server error
SERVICE_UNAVAILABLE: 503, // Temporary unavailability
};
// Example implementation
router.post('/users', async (req, res) => {
try {
const { email, password, name } = req.body;
// Validation
if (!email || !password || !name) {
return res.status(422).json({
success: false,
message: 'Validation failed',
errors: {
email: !email ? 'Email is required' : undefined,
password: !password ? 'Password is required' : undefined,
name: !name ? 'Name is required' : undefined,
}
});
}
// Check for existing user
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(409).json({
success: false,
message: 'User with this email already exists'
});
}
// Create user
const user = await User.create({ email, password, name });
res.status(201).json({
success: true,
data: user
});
} catch (error) {
res.status(500).json({
success: false,
message: 'Internal server error'
});
}
});Pagination and Filtering
Implement efficient data retrieval:
router.get('/products', async (req, res) => {
try {
// Pagination parameters
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
// Filtering
const filter = {};
if (req.query.category) {
filter.category = req.query.category;
}
if (req.query.minPrice) {
filter.price = { $gte: parseFloat(req.query.minPrice) };
}
if (req.query.maxPrice) {
filter.price = { ...filter.price, $lte: parseFloat(req.query.maxPrice) };
}
// Sorting
const sortBy = req.query.sortBy || 'createdAt';
const sortOrder = req.query.order === 'asc' ? 1 : -1;
const sort = { [sortBy]: sortOrder };
// Search
if (req.query.search) {
filter.$text = { $search: req.query.search };
}
// Execute query
const [products, total] = await Promise.all([
Product.find(filter)
.sort(sort)
.skip(skip)
.limit(limit),
Product.countDocuments(filter)
]);
// Response with metadata
res.json({
success: true,
data: products,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit)
}
});
} catch (error) {
res.status(500).json({ success: false, message: error.message });
}
});
// Example request:
// GET /products?page=2&limit=10&category=electronics&minPrice=100&sortBy=price&order=asc&search=laptopAPI Versioning
Future-proof your API:
const express = require('express');
const app = express();
// URL versioning (most common)
const v1Router = require('./routes/v1');
const v2Router = require('./routes/v2');
app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);
// Header versioning (alternative)
app.use('/api/users', (req, res, next) => {
const version = req.headers['api-version'] || '1';
if (version === '1') {
// V1 logic
res.json({ version: 1, users: [] });
} else if (version === '2') {
// V2 logic with additional fields
res.json({ version: 2, users: [], metadata: {} });
} else {
res.status(400).json({ error: 'Unsupported API version' });
}
});Error Handling
Consistent error response format:
// Error handler middleware
class ApiError extends Error {
constructor(statusCode, message, errors = null) {
super(message);
this.statusCode = statusCode;
this.errors = errors;
}
}
const errorHandler = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const response = {
success: false,
message: err.message || 'Internal server error',
};
if (err.errors) {
response.errors = err.errors;
}
if (process.env.NODE_ENV === 'development') {
response.stack = err.stack;
}
res.status(statusCode).json(response);
};
app.use(errorHandler);
// Usage in routes
router.post('/users', async (req, res, next) => {
try {
const { email } = req.body;
if (!email) {
throw new ApiError(422, 'Validation failed', {
email: 'Email is required'
});
}
const user = await User.create(req.body);
res.status(201).json({ success: true, data: user });
} catch (error) {
next(error);
}
});Authentication and Authorization
Secure your API endpoints:
const jwt = require('jsonwebtoken');
// Authentication middleware
const authenticate = async (req, res, next) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({
success: false,
message: 'Authentication required'
});
}
const decoded = jwt.verify(token, process.env.JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({
success: false,
message: 'Invalid authentication token'
});
}
req.user = user;
next();
} catch (error) {
res.status(401).json({
success: false,
message: 'Invalid authentication token'
});
}
};
// Authorization middleware
const authorize = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
success: false,
message: 'Insufficient permissions'
});
}
next();
};
};
// Usage
router.get('/admin/users',
authenticate,
authorize('admin', 'moderator'),
async (req, res) => {
const users = await User.find();
res.json({ success: true, data: users });
}
);Rate Limiting
Protect your API from abuse:
const rateLimit = require('express-rate-limit');
// Basic rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per windowMs
message: {
success: false,
message: 'Too many requests, please try again later.'
},
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// Stricter limit for authentication endpoints
const authLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 requests per hour
skipSuccessfulRequests: true,
message: {
success: false,
message: 'Too many login attempts, please try again later.'
}
});
app.use('/api/auth/login', authLimiter);Best Practices Checklist
// 1. Use nouns for resources, not verbs
// ✅ GET /articles
// ❌ GET /getArticles
// 2. Use plural nouns for collections
// ✅ GET /users
// ❌ GET /user
// 3. Use nested resources for relationships
// ✅ GET /users/123/posts
// ❌ GET /posts?userId=123
// 4. Return appropriate status codes
// 200: Success, 201: Created, 204: No Content
// 400: Bad Request, 401: Unauthorized, 404: Not Found
// 5. Include metadata in responses
{
"success": true,
"data": [],
"pagination": { "page": 1, "total": 100 },
"timestamp": "2025-09-10T10:00:00Z"
}
// 6. Use query parameters for filtering and sorting
// GET /products?category=electronics&sort=price&order=asc
// 7. Version your API
// /api/v1/users
// /api/v2/usersConclusion
Well-designed REST APIs are the backbone of modern web applications. Follow these principles to create APIs that are intuitive, maintainable, and scalable. Consistency is key—establish patterns and stick to them throughout your API.