REST API Design: The Complete Guide for Backend Developers
Description: A comprehensive guide to designing clean, scalable REST APIs — covering URL structure, authentication, versioning, validation, security, and testing with real-world Node.js examples.

REST API Design: The Complete Guide for Backend Developers
Poor API design costs companies thousands of hours in maintenance and frustrated developers. A well-designed API, on the other hand, becomes a competitive advantage that developers love to use.
Whether you're building internal microservices or public APIs used by millions, the principles of good API design remain the same. This guide will teach you how to design REST APIs that are intuitive, scalable, and maintainable.
By the end, you'll know how to structure endpoints, handle errors gracefully, implement authentication, version your APIs, and follow industry best practices used by companies like Stripe, GitHub, and Twilio.
Prerequisites
To get the most from this guide, you should have:
- Basic understanding of HTTP (GET, POST, status codes)
- Experience building backend services in any language
- Familiarity with JSON
- No advanced API knowledge required
What Makes a Good API?
A well-designed API is:
Intuitive: Developers can predict how it works without reading extensive documentation
Consistent: Similar operations follow similar patterns
Flexible: Supports current needs and future extensions
Secure: Protects data and prevents abuse
Well-Documented: Clear examples and explanations
Versioned: Changes don't break existing clients
REST Fundamentals
REST (Representational State Transfer) is an architectural style for designing networked applications. It relies on stateless, client-server communication using HTTP.
Core Principles
1. Resources Over Actions
Think in terms of resources (nouns), not actions (verbs).
❌ Bad:
POST /api/createUser
POST /api/getUser
POST /api/deleteUser
✅ Good:
POST /api/users (create user)
GET /api/users/:id (get user)
DELETE /api/users/:id (delete user)
2. Use HTTP Methods Correctly
GET - Retrieve resource(s)
POST - Create new resource
PUT - Replace entire resource
PATCH - Update part of resource
DELETE - Remove resource
3. Stateless Communication
Each request contains all information needed. Server doesn't store client state.
// ❌ Bad: Server-side sessions
GET /api/cart
Cookie: sessionId=abc123
// ✅ Good: Token in header
GET /api/users/123/cart
Authorization: Bearer eyJhbGc...
4. Use Standard HTTP Status Codes
2xx - Success
200 OK - Request successful
201 Created - Resource created
204 No Content - Success, no response body
3xx - Redirection
301 Moved Permanently
304 Not Modified
4xx - Client Errors
400 Bad Request - Invalid request
401 Unauthorized - Authentication required
403 Forbidden - Authenticated but not allowed
404 Not Found - Resource doesn't exist
429 Too Many Requests
5xx - Server Errors
500 Internal Server Error
503 Service Unavailable
URL Structure and Naming
Use Nouns for Resources
✅ Good:
/users
/products
/orders
/posts
/comments
❌ Bad:
/getUsers
/createProduct
/updateOrder
Plural vs Singular
Use plural for collections:
✅ Preferred:
GET /users (all users)
GET /users/123 (specific user)
❌ Inconsistent:
GET /user (all users?)
GET /user/123 (specific user)
Hierarchical Relationships
Show relationships through URL structure:
GET /users/123/posts (all posts by user 123)
GET /users/123/posts/456 (post 456 by user 123)
GET /posts/456/comments (comments on post 456)
POST /posts/456/comments (add comment to post 456)
Use Hyphens, Not Underscores
✅ Good:
/api/user-profiles
/api/order-history
❌ Bad:
/api/user_profiles
/api/order_history
Keep URLs Lowercase
✅ Good:
/api/products/electronics
❌ Bad:
/api/Products/Electronics
/api/PRODUCTS/ELECTRONICS
Endpoint Design Patterns
CRUD Operations
Standard pattern for Create, Read, Update, Delete:
// Express.js example
// List all users (with pagination)
app.get('/api/users', async (req, res) => {
const { page = 1, limit = 20 } = req.query;
const users = await User.find()
.limit(limit)
.skip((page - 1) * limit);
const total = await User.countDocuments();
res.json({
data: users,
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
// Get single user
app.get('/api/users/:id', async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: 'User not found'
}
});
}
res.json({ data: user });
});
// Create user
app.post('/api/users', async (req, res) => {
const { name, email, password } = req.body;
// Validation
if (!email || !password) {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Email and password required',
fields: {
email: !email ? 'Required' : undefined,
password: !password ? 'Required' : undefined
}
}
});
}
const user = await User.create({ name, email, password });
res.status(201).json({ data: user });
});
// Update user (full replacement)
app.put('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, overwrite: true }
);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: 'User not found'
}
});
}
res.json({ data: user });
});
// Update user (partial update)
app.patch('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true }
);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: 'User not found'
}
});
}
res.json({ data: user });
});
// Delete user
app.delete('/api/users/:id', async (req, res) => {
const user = await User.findByIdAndDelete(req.params.id);
if (!user) {
return res.status(404).json({
error: {
code: 'USER_NOT_FOUND',
message: 'User not found'
}
});
}
res.status(204).send();
});
Filtering and Searching
// Filtering with query parameters
app.get('/api/products', async (req, res) => {
const {
category,
minPrice,
maxPrice,
inStock,
search
} = req.query;
const query = {};
if (category) {
query.category = category;
}
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = parseFloat(minPrice);
if (maxPrice) query.price.$lte = parseFloat(maxPrice);
}
if (inStock !== undefined) {
query.inStock = inStock === 'true';
}
if (search) {
query.$text = { $search: search };
}
const products = await Product.find(query);
res.json({ data: products });
});
// Usage:
// GET /api/products?category=electronics&minPrice=100&maxPrice=500&inStock=true
// GET /api/products?search=laptop
Sorting
app.get('/api/products', async (req, res) => {
const { sortBy = 'createdAt', order = 'desc' } = req.query;
const sortOrder = order === 'asc' ? 1 : -1;
const sortOptions = { [sortBy]: sortOrder };
const products = await Product.find().sort(sortOptions);
res.json({ data: products });
});
// Usage:
// GET /api/products?sortBy=price&order=asc
// GET /api/products?sortBy=name&order=desc
Pagination
// Offset-based pagination
app.get('/api/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const skip = (page - 1) * limit;
const posts = await Post.find()
.limit(limit)
.skip(skip)
.sort({ createdAt: -1 });
const total = await Post.countDocuments();
res.json({
data: posts,
meta: {
page,
limit,
total,
totalPages: Math.ceil(total / limit),
hasNext: page * limit < total,
hasPrev: page > 1
}
});
});
// Cursor-based pagination (better for large datasets)
app.get('/api/posts', async (req, res) => {
const { cursor, limit = 20 } = req.query;
const query = cursor ? { _id: { $lt: cursor } } : {};
const posts = await Post.find(query)
.limit(parseInt(limit) + 1)
.sort({ _id: -1 });
const hasNext = posts.length > limit;
const data = hasNext ? posts.slice(0, -1) : posts;
const nextCursor = hasNext ? posts[posts.length - 2]._id : null;
res.json({
data,
meta: {
nextCursor,
hasNext
}
});
});
// Usage:
// GET /api/posts?limit=20
// GET /api/posts?cursor=507f1f77bcf86cd799439011&limit=20
Field Selection
Allow clients to request specific fields:
app.get('/api/users/:id', async (req, res) => {
const { fields } = req.query;
let query = User.findById(req.params.id);
if (fields) {
// fields=name,email,createdAt
const selectedFields = fields.split(',').join(' ');
query = query.select(selectedFields);
}
const user = await query;
res.json({ data: user });
});
// Usage:
// GET /api/users/123?fields=name,email
Nested Resources
// Get user's orders
app.get('/api/users/:userId/orders', async (req, res) => {
const orders = await Order.find({ userId: req.params.userId });
res.json({ data: orders });
});
// Get specific order for user
app.get('/api/users/:userId/orders/:orderId', async (req, res) => {
const order = await Order.findOne({
_id: req.params.orderId,
userId: req.params.userId
});
if (!order) {
return res.status(404).json({
error: {
code: 'ORDER_NOT_FOUND',
message: 'Order not found'
}
});
}
res.json({ data: order });
});
// Create order for user
app.post('/api/users/:userId/orders', async (req, res) => {
const order = await Order.create({
userId: req.params.userId,
...req.body
});
res.status(201).json({ data: order });
});
Response Format Standards
Consistent Response Structure
Use a consistent envelope for all responses:
// Success response
{
"data": {
"id": "123",
"name": "John Doe",
"email": "[email protected]"
}
}
// List response
{
"data": [
{ "id": "1", "name": "Item 1" },
{ "id": "2", "name": "Item 2" }
],
"meta": {
"page": 1,
"limit": 20,
"total": 50
}
}
// Error response
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid email format",
"details": {
"field": "email",
"value": "invalid-email"
}
}
}
Standardized Error Format
class APIError extends Error {
constructor(statusCode, code, message, details = null) {
super(message);
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
// Error handler middleware
app.use((err, req, res, next) => {
if (err instanceof APIError) {
return res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
details: err.details
}
});
}
// Unexpected errors
console.error(err);
res.status(500).json({
error: {
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred'
}
});
});
// Usage
app.post('/api/users', async (req, res, next) => {
try {
const { email } = req.body;
if (!isValidEmail(email)) {
throw new APIError(
400,
'VALIDATION_ERROR',
'Invalid email format',
{ field: 'email', value: email }
);
}
const existingUser = await User.findOne({ email });
if (existingUser) {
throw new APIError(
409,
'USER_EXISTS',
'User with this email already exists'
);
}
const user = await User.create(req.body);
res.status(201).json({ data: user });
} catch (err) {
next(err);
}
});
Include Metadata
// Pagination metadata
{
"data": [...],
"meta": {
"page": 2,
"limit": 20,
"total": 156,
"totalPages": 8
}
}
// Timestamp metadata
{
"data": {...},
"meta": {
"requestId": "req_abc123",
"timestamp": "2026-04-22T10:30:00Z",
"version": "v1"
}
}
// Links for navigation (HATEOAS)
{
"data": [...],
"links": {
"self": "/api/users?page=2",
"first": "/api/users?page=1",
"prev": "/api/users?page=1",
"next": "/api/users?page=3",
"last": "/api/users?page=8"
}
}
Authentication and Authorization
API Keys
Simple but less secure. Good for server-to-server communication.
const API_KEYS = {
'key_abc123': { name: 'Mobile App', permissions: ['read', 'write'] },
'key_xyz789': { name: 'Analytics', permissions: ['read'] }
};
function authenticateAPIKey(req, res, next) {
const apiKey = req.headers['x-api-key'];
if (!apiKey || !API_KEYS[apiKey]) {
return res.status(401).json({
error: {
code: 'INVALID_API_KEY',
message: 'Invalid or missing API key'
}
});
}
req.apiClient = API_KEYS[apiKey];
next();
}
app.use('/api', authenticateAPIKey);
JWT (JSON Web Tokens)
Industry standard for stateless authentication.
const jwt = require('jsonwebtoken');
// Generate token
app.post('/api/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({
error: {
code: 'INVALID_CREDENTIALS',
message: 'Invalid email or password'
}
});
}
const token = jwt.sign(
{
userId: user._id,
email: user.email,
role: user.role
},
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
data: {
token,
user: {
id: user._id,
name: user.name,
email: user.email
}
}
});
});
// Verify token middleware
function authenticateJWT(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({
error: {
code: 'MISSING_TOKEN',
message: 'Authorization token required'
}
});
}
const token = authHeader.substring(7);
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({
error: {
code: 'INVALID_TOKEN',
message: 'Invalid or expired token'
}
});
}
}
// Protected route
app.get('/api/profile', authenticateJWT, async (req, res) => {
const user = await User.findById(req.user.userId);
res.json({ data: user });
});
Role-Based Access Control
function authorize(...allowedRoles) {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
error: {
code: 'UNAUTHORIZED',
message: 'Authentication required'
}
});
}
if (!allowedRoles.includes(req.user.role)) {
return res.status(403).json({
error: {
code: 'FORBIDDEN',
message: 'Insufficient permissions'
}
});
}
next();
};
}
// Usage
app.delete(
'/api/users/:id',
authenticateJWT,
authorize('admin', 'moderator'),
async (req, res) => {
// Only admins and moderators can delete users
await User.findByIdAndDelete(req.params.id);
res.status(204).send();
}
);
OAuth 2.0
For third-party integrations:
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/api/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName
});
}
done(null, user);
}
));
app.get('/api/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/api/auth/google/callback',
passport.authenticate('google', { session: false }),
(req, res) => {
const token = jwt.sign(
{ userId: req.user._id },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.redirect(`/auth/success?token=${token}`);
}
);
Rate Limiting
Protect your API from abuse:
const rateLimit = require('express-rate-limit');
// Global rate limit
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window
message: {
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests, please try again later'
}
},
standardHeaders: true, // Return rate limit info in headers
legacyHeaders: false
});
app.use('/api', globalLimiter);
// Endpoint-specific rate limit
const loginLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 5, // 5 attempts per hour
skipSuccessfulRequests: true
});
app.post('/api/auth/login', loginLimiter, async (req, res) => {
// Login logic
});
// Custom rate limiter with Redis
const Redis = require('ioredis');
const redis = new Redis();
async function rateLimitMiddleware(req, res, next) {
const key = `rate_limit:${req.ip}`;
const limit = 100;
const window = 60; // seconds
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, window);
}
if (current > limit) {
return res.status(429).json({
error: {
code: 'RATE_LIMIT_EXCEEDED',
message: 'Too many requests'
}
});
}
// Add rate limit headers
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', limit - current);
next();
}
Versioning Strategies
URL Versioning (Most Common)
// Version in URL path
app.use('/api/v1', v1Routes);
app.use('/api/v2', v2Routes);
// v1/users.js
router.get('/users/:id', (req, res) => {
// Old implementation
res.json({
id: user.id,
name: user.name
});
});
// v2/users.js
router.get('/users/:id', (req, res) => {
// New implementation with more fields
res.json({
id: user.id,
firstName: user.firstName,
lastName: user.lastName,
fullName: `${user.firstName} ${user.lastName}`
});
});
Header Versioning
function versionMiddleware(req, res, next) {
const version = req.headers['api-version'] || '1';
req.apiVersion = version;
next();
}
app.use(versionMiddleware);
app.get('/api/users/:id', (req, res) => {
if (req.apiVersion === '2') {
// v2 response
res.json({ firstName: user.firstName, lastName: user.lastName });
} else {
// v1 response
res.json({ name: user.name });
}
});
Content Negotiation
app.get('/api/users/:id', (req, res) => {
const accept = req.headers['accept'];
if (accept.includes('application/vnd.myapi.v2+json')) {
// v2 response
res.json({ firstName: user.firstName, lastName: user.lastName });
} else {
// v1 response
res.json({ name: user.name });
}
});
Request Validation
Using Joi
const Joi = require('joi');
const createUserSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
age: Joi.number().integer().min(18).max(120).optional(),
role: Joi.string().valid('user', 'admin').default('user')
});
function validateRequest(schema) {
return (req, res, next) => {
const { error, value } = schema.validate(req.body, {
abortEarly: false, // Return all errors
stripUnknown: true // Remove unknown fields
});
if (error) {
const errors = error.details.reduce((acc, err) => {
acc[err.path[0]] = err.message;
return acc;
}, {});
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
fields: errors
}
});
}
req.body = value;
next();
};
}
app.post('/api/users', validateRequest(createUserSchema), async (req, res) => {
const user = await User.create(req.body);
res.status(201).json({ data: user });
});
Using Express Validator
const { body, validationResult } = require('express-validator');
app.post('/api/users',
[
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 8 }),
body('name').trim().notEmpty(),
body('age').optional().isInt({ min: 18, max: 120 })
],
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: {
code: 'VALIDATION_ERROR',
message: 'Request validation failed',
fields: errors.mapped()
}
});
}
// Proceed with user creation
}
);
Documentation
OpenAPI/Swagger
const swaggerJsdoc = require('swagger-jsdoc');
const swaggerUi = require('swagger-ui-express');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'My API',
version: '1.0.0',
description: 'A sample API'
},
servers: [
{
url: 'http://localhost:3000',
description: 'Development server'
}
]
},
apis: ['./routes/*.js']
};
const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));
/**
* @swagger
* /api/users:
* get:
* summary: Retrieve a list of users
* tags: [Users]
* parameters:
* - in: query
* name: page
* schema:
* type: integer
* description: Page number
* - in: query
* name: limit
* schema:
* type: integer
* description: Number of items per page
* responses:
* 200:
* description: A list of users
* content:
* application/json:
* schema:
* type: object
* properties:
* data:
* type: array
* items:
* $ref: '#/components/schemas/User'
* meta:
* type: object
*/
app.get('/api/users', async (req, res) => {
// Implementation
});
/**
* @swagger
* components:
* schemas:
* User:
* type: object
* required:
* - name
* - email
* properties:
* id:
* type: string
* name:
* type: string
* email:
* type: string
* format: email
* createdAt:
* type: string
* format: date-time
*/
Performance Optimization
Caching
const redis = require('redis');
const client = redis.createClient();
function cache(duration) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// Store original send
res.originalSend = res.send;
// Override send
res.send = function(data) {
res.originalSend(data);
client.setex(key, duration, data);
};
next();
} catch (err) {
next();
}
};
}
// Usage
app.get('/api/products', cache(300), async (req, res) => {
const products = await Product.find();
res.json({ data: products });
});
Compression
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6 // Compression level (0-9)
}));
Database Query Optimization
// Bad: N+1 query problem
app.get('/api/posts', async (req, res) => {
const posts = await Post.find();
for (let post of posts) {
post.author = await User.findById(post.authorId); // N queries!
}
res.json({ data: posts });
});
// Good: Use populate/join
app.get('/api/posts', async (req, res) => {
const posts = await Post.find().populate('author'); // 1 query
res.json({ data: posts });
});
// Good: Use projection to select only needed fields
app.get('/api/users', async (req, res) => {
const users = await User.find()
.select('name email -_id') // Only name and email, exclude _id
.lean(); // Return plain objects, not Mongoose documents
res.json({ data: users });
});
Security Best Practices
Input Sanitization
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
// Prevent NoSQL injection
app.use(mongoSanitize());
// Prevent XSS attacks
app.use(xss());
// Helmet for security headers
const helmet = require('helmet');
app.use(helmet());
CORS Configuration
const cors = require('cors');
app.use(cors({
origin: process.env.ALLOWED_ORIGINS.split(','),
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400 // 24 hours
}));
SQL Injection Prevention
// ❌ Bad: Vulnerable to SQL injection
app.get('/api/users', (req, res) => {
const { name } = req.query;
const query = `SELECT * FROM users WHERE name = '${name}'`;
db.query(query, (err, results) => {
res.json({ data: results });
});
});
// ✅ Good: Use parameterized queries
app.get('/api/users', (req, res) => {
const { name } = req.query;
const query = 'SELECT * FROM users WHERE name = ?';
db.query(query, [name], (err, results) => {
res.json({ data: results });
});
});
Testing APIs
Unit Tests
const request = require('supertest');
const app = require('../app');
describe('User API', () => {
describe('GET /api/users/:id', () => {
it('should return user when ID is valid', async () => {
const res = await request(app)
.get('/api/users/123')
.expect(200);
expect(res.body.data).toHaveProperty('id', '123');
expect(res.body.data).toHaveProperty('name');
});
it('should return 404 when user not found', async () => {
const res = await request(app)
.get('/api/users/999')
.expect(404);
expect(res.body.error.code).toBe('USER_NOT_FOUND');
});
});
describe('POST /api/users', () => {
it('should create user with valid data', async () => {
const userData = {
name: 'John Doe',
email: '[email protected]',
password: 'password123'
};
const res = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(res.body.data).toHaveProperty('id');
expect(res.body.data.email).toBe(userData.email);
});
it('should return 400 with invalid email', async () => {
const userData = {
name: 'John Doe',
email: 'invalid-email',
password: 'password123'
};
const res = await request(app)
.post('/api/users')
.send(userData)
.expect(400);
expect(res.body.error.code).toBe('VALIDATION_ERROR');
});
});
});
Integration Tests
describe('Order Flow Integration', () => {
let authToken;
let userId;
let productId;
beforeAll(async () => {
// Create test user and login
const user = await request(app)
.post('/api/users')
.send({
name: 'Test User',
email: '[email protected]',
password: 'password123'
});
userId = user.body.data.id;
const login = await request(app)
.post('/api/auth/login')
.send({
email: '[email protected]',
password: 'password123'
});
authToken = login.body.data.token;
// Create test product
const product = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${authToken}`)
.send({
name: 'Test Product',
price: 99.99
});
productId = product.body.data.id;
});
it('should complete full order flow', async () => {
// Add to cart
await request(app)
.post(`/api/users/${userId}/cart`)
.set('Authorization', `Bearer ${authToken}`)
.send({ productId, quantity: 2 })
.expect(201);
// Create order
const order = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${authToken}`)
.send({
items: [{ productId, quantity: 2 }],
shippingAddress: '123 Main St'
})
.expect(201);
expect(order.body.data).toHaveProperty('id');
expect(order.body.data.total).toBe(199.98);
// Verify order was created
const getOrder = await request(app)
.get(`/api/orders/${order.body.data.id}`)
.set('Authorization', `Bearer ${authToken}`)
.expect(200);
expect(getOrder.body.data.status).toBe('pending');
});
});
Monitoring and Logging
const winston = require('winston');
const morgan = require('morgan');
// Configure logger
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// HTTP request logging
app.use(morgan('combined', {
stream: {
write: (message) => logger.info(message.trim())
}
}));
// Request ID middleware
app.use((req, res, next) => {
req.id = require('uuid').v4();
res.setHeader('X-Request-ID', req.id);
next();
});
// Log all API requests
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
logger.info({
requestId: req.id,
method: req.method,
url: req.url,
statusCode: res.statusCode,
duration: Date.now() - start,
userAgent: req.headers['user-agent']
});
});
next();
});
// Error logging
app.use((err, req, res, next) => {
logger.error({
requestId: req.id,
error: {
message: err.message,
stack: err.stack
},
request: {
method: req.method,
url: req.url,
headers: req.headers,
body: req.body
}
});
next(err);
});
Real-World Example: E-commerce API
Complete implementation:
const express = require('express');
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const app = express();
app.use(express.json());
// Models
const UserSchema = new mongoose.Schema({
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['user', 'admin'], default: 'user' }
});
const ProductSchema = new mongoose.Schema({
name: { type: String, required: true },
price: { type: Number, required: true },
stock: { type: Number, default: 0 },
category: String,
description: String
});
const OrderSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
items: [{
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
quantity: Number,
price: Number
}],
total: Number,
status: {
type: String,
enum: ['pending', 'processing', 'shipped', 'delivered'],
default: 'pending'
},
createdAt: { type: Date, default: Date.now }
});
const User = mongoose.model('User', UserSchema);
const Product = mongoose.model('Product', ProductSchema);
const Order = mongoose.model('Order', OrderSchema);
// Auth endpoints
app.post('/api/auth/register', async (req, res) => {
try {
const { email, password } = req.body;
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
email,
password: hashedPassword
});
const token = jwt.sign(
{ userId: user._id, role: user.role },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({
data: {
token,
user: { id: user._id, email: user.email }
}
});
} catch (err) {
res.status(400).json({
error: { code: 'REGISTRATION_FAILED', message: err.message }
});
}
});
// Product endpoints
app.get('/api/products', async (req, res) => {
const { page = 1, limit = 20, category, minPrice, maxPrice } = req.query;
const query = {};
if (category) query.category = category;
if (minPrice || maxPrice) {
query.price = {};
if (minPrice) query.price.$gte = parseFloat(minPrice);
if (maxPrice) query.price.$lte = parseFloat(maxPrice);
}
const products = await Product.find(query)
.limit(limit)
.skip((page - 1) * limit);
const total = await Product.countDocuments(query);
res.json({
data: products,
meta: {
page: parseInt(page),
limit: parseInt(limit),
total,
pages: Math.ceil(total / limit)
}
});
});
app.post('/api/products', authenticateJWT, authorize('admin'), async (req, res) => {
const product = await Product.create(req.body);
res.status(201).json({ data: product });
});
// Order endpoints
app.post('/api/orders', authenticateJWT, async (req, res) => {
const { items } = req.body;
// Calculate total
let total = 0;
const orderItems = [];
for (const item of items) {
const product = await Product.findById(item.productId);
if (!product || product.stock < item.quantity) {
return res.status(400).json({
error: {
code: 'INSUFFICIENT_STOCK',
message: `Insufficient stock for product ${product.name}`
}
});
}
orderItems.push({
productId: product._id,
quantity: item.quantity,
price: product.price
});
total += product.price * item.quantity;
// Reduce stock
product.stock -= item.quantity;
await product.save();
}
const order = await Order.create({
userId: req.user.userId,
items: orderItems,
total
});
res.status(201).json({ data: order });
});
app.get('/api/orders', authenticateJWT, async (req, res) => {
const orders = await Order.find({ userId: req.user.userId })
.populate('items.productId', 'name price')
.sort({ createdAt: -1 });
res.json({ data: orders });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Conclusion
Designing great REST APIs is both an art and a science. The principles in this guide—intuitive URLs, consistent responses, proper error handling, security, and documentation—form the foundation of APIs that developers love to use.
Remember that API design is iterative. Start with the basics, get feedback from users, and continuously improve. The best APIs are shaped by real-world use cases and developer experience.
Quick Checklist
Before releasing your API, verify:
- URLs follow RESTful conventions
- HTTP methods used correctly
- Consistent response format
- Comprehensive error handling
- Authentication and authorization implemented
- Rate limiting in place
- Input validation on all endpoints
- API versioning strategy
- Documentation complete
- Tests cover critical paths
- Logging and monitoring configured
- Security headers set
- CORS configured properly
Resources
Tools:
- Postman - API testing
- Swagger/OpenAPI - Documentation
- Insomnia - API client
- Newman - Automated testing
Libraries:
- Express.js - Node.js framework
- Fastify - Fast Node.js framework
- Django REST - Python framework
- Spring Boot - Java framework
Books:
- "RESTful Web APIs" by Leonard Richardson
- "API Design Patterns" by JJ Geewax
Start building better APIs today. Your developers will thank you.