← Back to Guides

RESTful API Design Best Practices

📖 14 min read | 📅 Updated: January 2025 | 🏷️ Backend & APIs

Introduction

RESTful API design is crucial for building scalable, maintainable, and intuitive web services. This guide covers HTTP methods, status codes, resource naming, versioning, pagination, filtering, and advanced patterns for professional API development.

1. Resource Naming Conventions

URL Structure Best Practices

✓ GOOD Examples:
GET    /api/users                    # List all users
GET    /api/users/123                # Get specific user
GET    /api/users/123/posts          # User's posts
GET    /api/posts?author=123         # Posts filtered by author
POST   /api/users                    # Create user
PUT    /api/users/123                # Update entire user
PATCH  /api/users/123                # Partial update
DELETE /api/users/123                # Delete user

✗ BAD Examples:
GET    /api/getUsers                 # Verb in URL
GET    /api/user/123                 # Singular noun
POST   /api/users/create             # Redundant verb
GET    /api/users/123/getPosts       # Verb in URL
DELETE /api/deleteUser?id=123        # Verb + query param

Guidelines:
- Use plural nouns for resources
- Use nouns, not verbs
- Use lowercase with hyphens for multi-word
- Nested resources for relationships
- Maximum 2-3 levels of nesting

2. HTTP Methods

Standard HTTP Verbs

Method Purpose Idempotent Safe
GET Retrieve resource(s) Yes Yes
POST Create new resource No No
PUT Replace entire resource Yes No
PATCH Partial update No* No
DELETE Remove resource Yes No
HEAD Get headers only Yes Yes
OPTIONS Get allowed methods Yes Yes

Implementation Examples

// Express.js REST endpoints
import express from 'express';
const router = express.Router();

// GET - Retrieve all
router.get('/users', async (req, res) => {
    const users = await User.find();
    res.json({ data: users });
});

// GET - Retrieve one
router.get('/users/:id', async (req, res) => {
    const user = await User.findById(req.params.id);
    if (!user) return res.status(404).json({ error: 'User not found' });
    res.json({ data: user });
});

// POST - Create
router.post('/users', async (req, res) => {
    const user = await User.create(req.body);
    res.status(201).json({ data: user });
});

// PUT - Full replacement
router.put('/users/:id', async (req, res) => {
    const user = await User.findByIdAndUpdate(
        req.params.id,
        req.body,
        { new: true, overwrite: true }
    );
    res.json({ data: user });
});

// PATCH - Partial update
router.patch('/users/:id', async (req, res) => {
    const user = await User.findByIdAndUpdate(
        req.params.id,
        { $set: req.body },
        { new: true }
    );
    res.json({ data: user });
});

// DELETE
router.delete('/users/:id', async (req, res) => {
    await User.findByIdAndDelete(req.params.id);
    res.status(204).send();
});

3. HTTP Status Codes

Common Status Codes

2xx Success:
200 OK              - Successful GET, PUT, PATCH
201 Created         - Successful POST
204 No Content      - Successful DELETE
206 Partial Content - Pagination/chunked response

3xx Redirection:
301 Moved Permanently  - Resource URL changed
304 Not Modified       - Cached version valid

4xx Client Errors:
400 Bad Request        - Invalid syntax/validation error
401 Unauthorized       - Authentication required
403 Forbidden          - No permission
404 Not Found          - Resource doesn't exist
405 Method Not Allowed - Wrong HTTP method
409 Conflict           - Resource conflict (duplicate)
422 Unprocessable      - Semantic errors
429 Too Many Requests  - Rate limit exceeded

5xx Server Errors:
500 Internal Server Error - Generic server error
502 Bad Gateway           - Upstream server error
503 Service Unavailable   - Temporary unavailable
504 Gateway Timeout       - Upstream timeout

Error Response Format

// Consistent error response structure
{
    "error": {
        "code": "VALIDATION_ERROR",
        "message": "Invalid input data",
        "details": [
            {
                "field": "email",
                "message": "Email is required"
            },
            {
                "field": "password",
                "message": "Password must be at least 8 characters"
            }
        ],
        "timestamp": "2025-01-15T10:30:00Z",
        "path": "/api/users",
        "requestId": "abc-123-def-456"
    }
}

4. Pagination

Pagination Strategies

// 1. Offset-based pagination
GET /api/users?page=2&limit=20

// Implementation
router.get('/users', async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 20;
    const skip = (page - 1) * limit;
    
    const [users, total] = await Promise.all([
        User.find().skip(skip).limit(limit),
        User.countDocuments()
    ]);
    
    res.json({
        data: users,
        pagination: {
            page,
            limit,
            total,
            pages: Math.ceil(total / limit),
            hasNext: page * limit < total,
            hasPrev: page > 1
        }
    });
});

// 2. Cursor-based pagination (better for large datasets)
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20

router.get('/users', async (req, res) => {
    const limit = parseInt(req.query.limit) || 20;
    const cursor = req.query.cursor 
        ? JSON.parse(Buffer.from(req.query.cursor, 'base64').toString())
        : null;
    
    const query = cursor ? { _id: { $gt: cursor.id } } : {};
    const users = await User.find(query).limit(limit + 1);
    
    const hasMore = users.length > limit;
    const items = hasMore ? users.slice(0, -1) : users;
    const nextCursor = hasMore 
        ? Buffer.from(JSON.stringify({ id: items[items.length - 1]._id })).toString('base64')
        : null;
    
    res.json({
        data: items,
        pagination: {
            nextCursor,
            hasMore
        }
    });
});

5. Filtering, Sorting & Searching

// Filtering
GET /api/products?category=electronics&price[gte]=100&price[lte]=500

// Sorting
GET /api/products?sort=-price,name  // -price desc, name asc

// Searching
GET /api/products?search=laptop&fields=name,description

// Field selection
GET /api/users?fields=name,email,-password

// Implementation
router.get('/products', async (req, res) => {
    // Build filter
    const filter = {};
    if (req.query.category) filter.category = req.query.category;
    if (req.query['price[gte]']) filter.price = { $gte: req.query['price[gte]'] };
    if (req.query['price[lte]']) filter.price = { ...filter.price, $lte: req.query['price[lte]'] };
    
    // Build sort
    const sort = {};
    if (req.query.sort) {
        req.query.sort.split(',').forEach(field => {
            if (field.startsWith('-')) {
                sort[field.slice(1)] = -1;
            } else {
                sort[field] = 1;
            }
        });
    }
    
    // Search
    if (req.query.search) {
        filter.$text = { $search: req.query.search };
    }
    
    // Field selection
    let select = '';
    if (req.query.fields) {
        select = req.query.fields.replace(/,/g, ' ');
    }
    
    const products = await Product.find(filter)
        .sort(sort)
        .select(select)
        .limit(20);
    
    res.json({ data: products });
});

6. API Versioning

Versioning Strategies

// 1. URL Versioning (Recommended)
GET /api/v1/users
GET /api/v2/users

// 2. Header Versioning
GET /api/users
Accept: application/vnd.myapi.v2+json

// 3. Query Parameter
GET /api/users?version=2

// Express implementation - URL versioning
import express from 'express';

// V1 routes
const v1Router = express.Router();
v1Router.get('/users', (req, res) => {
    res.json({ version: 'v1', users: [] });
});

// V2 routes  
const v2Router = express.Router();
v2Router.get('/users', (req, res) => {
    res.json({ version: 'v2', users: [], newField: true });
});

app.use('/api/v1', v1Router);
app.use('/api/v2', v2Router);

// Deprecation header
app.use('/api/v1', (req, res, next) => {
    res.set('X-API-Warn', 'API v1 is deprecated. Please migrate to v2.');
    next();
});

7. HATEOAS (Hypermedia)

// Include links to related resources
{
    "data": {
        "id": "123",
        "name": "John Doe",
        "email": "john@example.com"
    },
    "links": {
        "self": "/api/users/123",
        "posts": "/api/users/123/posts",
        "followers": "/api/users/123/followers",
        "following": "/api/users/123/following"
    },
    "actions": [
        {
            "name": "update",
            "method": "PATCH",
            "href": "/api/users/123"
        },
        {
            "name": "delete",
            "method": "DELETE",
            "href": "/api/users/123"
        }
    ]
}

8. Rate Limiting Headers

// Standard rate limit headers
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 75
X-RateLimit-Reset: 1642521600

// Implementation
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100,
    standardHeaders: true,
    handler: (req, res) => {
        res.status(429).json({
            error: {
                code: 'RATE_LIMIT_EXCEEDED',
                message: 'Too many requests, please try again later',
                retryAfter: req.rateLimit.resetTime
            }
        });
    }
});

9. Content Negotiation

// Support multiple response formats
GET /api/users
Accept: application/json        # JSON response
Accept: application/xml         # XML response
Accept: text/csv                # CSV response

// Express implementation
router.get('/users', async (req, res) => {
    const users = await User.find();
    
    res.format({
        'application/json': () => {
            res.json({ data: users });
        },
        'application/xml': () => {
            const xml = `
                ${users.map(u => `${u.id}`).join('')}`;
            res.send(xml);
        },
        'text/csv': () => {
            const csv = users.map(u => `${u.id},${u.name},${u.email}`).join('\n');
            res.send(csv);
        },
        'default': () => {
            res.status(406).send('Not Acceptable');
        }
    });
});

10. API Documentation

// OpenAPI/Swagger specification
/**
 * @swagger
 * /api/users:
 *   get:
 *     summary: Get all users
 *     tags: [Users]
 *     parameters:
 *       - in: query
 *         name: page
 *         schema:
 *           type: integer
 *         description: Page number
 *     responses:
 *       200:
 *         description: List of users
 *         content:
 *           application/json:
 *             schema:
 *               type: object
 *               properties:
 *                 data:
 *                   type: array
 *                   items:
 *                     $ref: '#/components/schemas/User'
 */

// Install swagger
npm install swagger-jsdoc swagger-ui-express

// Setup
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';

const options = {
    definition: {
        openapi: '3.0.0',
        info: {
            title: 'My API',
            version: '1.0.0',
        },
    },
    apis: ['./routes/*.ts'],
};

const specs = swaggerJsdoc(options);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs));

Best Practices Checklist

✓ RESTful API Design Checklist:

Conclusion

Well-designed REST APIs are intuitive, consistent, and easy to integrate. By following HTTP standards, using proper status codes, implementing pagination and filtering, and maintaining good documentation, you create APIs that developers love to use and that scale effectively.

💡 Pro Tip: Consistency is more important than perfection. Choose conventions (naming, error formats, pagination style) and stick to them throughout your API. Document deviations and always think about the developer experience.