6 min read By Michael Rodriguez

API Design: Building RESTful APIs That Last

Learn the principles of designing robust, scalable RESTful APIs that are easy to use and maintain for years to come.

API REST Backend Web Development
API Design: Building RESTful APIs That Last

What Makes a Good API?

A well-designed API is consistent, intuitive, and built to last. It should be easy for developers to understand and use without extensive documentation.

RESTful Principles

Resource-Based URLs

# Good - Resource-oriented
GET    /api/users
GET    /api/users/123
POST   /api/users
PUT    /api/users/123
DELETE /api/users/123

# Bad - Action-oriented
GET    /api/getUsers
GET    /api/getUserById/123
POST   /api/createUser
POST   /api/updateUser/123
POST   /api/deleteUser/123

Use HTTP Methods Correctly

GET     # Retrieve resource(s) - Safe & Idempotent
POST    # Create new resource - Not idempotent
PUT     # Update/replace resource - Idempotent
PATCH   # Partial update - Not idempotent
DELETE  # Remove resource - Idempotent

HTTP Status Codes

// Success
200 OK              // Successful GET, PUT, PATCH, DELETE
201 Created         // Successful POST
204 No Content      // Successful DELETE with no body

// Client Errors
400 Bad Request     // Invalid request data
401 Unauthorized    // Missing or invalid auth
403 Forbidden       // Authenticated but not allowed
404 Not Found       // Resource doesn't exist
409 Conflict        // Duplicate resource
422 Unprocessable   // Validation error
429 Too Many        // Rate limit exceeded

// Server Errors
500 Internal Error  // Server error
502 Bad Gateway     // Upstream error
503 Service Unavail // Server down

URL Structure Best Practices

Naming Conventions

# Use plural nouns
GET /api/products
GET /api/products/123

# Nested resources
GET /api/users/123/posts
GET /api/posts/456/comments

# Actions on resources (when needed)
POST /api/users/123/activate
POST /api/orders/456/cancel

# Filtering, sorting, pagination
GET /api/products?category=electronics&sort=-price&page=2&limit=20

Versioning

# URL versioning (recommended)
GET /api/v1/users
GET /api/v2/users

# Header versioning
GET /api/users
Accept: application/vnd.myapi.v1+json

# Query parameter versioning
GET /api/users?version=1

Request/Response Design

Request Body

// POST /api/users
{
  "email": "user@example.com",
  "name": "John Doe",
  "role": "user"
}

// PUT /api/users/123
{
  "email": "newemail@example.com",
  "name": "John Smith",
  "role": "admin"
}

// PATCH /api/users/123
{
  "name": "John Smith"
}

Response Body

// GET /api/users/123
{
  "id": "123",
  "email": "user@example.com",
  "name": "John Doe",
  "role": "user",
  "createdAt": "2024-01-15T10:30:00Z",
  "updatedAt": "2024-02-20T14:20:00Z"
}

// GET /api/users (Collection)
{
  "data": [
    { "id": "123", "name": "John" },
    { "id": "124", "name": "Jane" }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "perPage": 20,
    "totalPages": 8
  },
  "links": {
    "self": "/api/users?page=1",
    "next": "/api/users?page=2",
    "last": "/api/users?page=8"
  }
}

Error Responses

// Consistent error structure
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters"
      }
    ],
    "requestId": "abc-123-def",
    "timestamp": "2024-02-20T14:30:00Z"
  }
}

Authentication & Authorization

JWT Authentication

// Request
POST /api/auth/login
{
  "email": "user@example.com",
  "password": "secret123"
}

// Response
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refreshToken": "abc123def456",
  "expiresIn": 3600
}

// Authenticated request
GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API Keys

// Header
GET /api/data
X-API-Key: sk_live_abc123def456

// Query parameter (less secure)
GET /api/data?api_key=sk_live_abc123def456

Pagination

Offset-Based

GET /api/posts?page=2&limit=20

# Response
{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 150,
    "totalPages": 8
  }
}

Cursor-Based

GET /api/posts?cursor=eyJpZCI6MTIzfQ&limit=20

# Response
{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6MTQzfQ",
    "hasMore": true
  }
}

Filtering & Sorting

# Filtering
GET /api/products?category=electronics&price_min=100&price_max=500

# Sorting (prefix - for descending)
GET /api/posts?sort=-createdAt,title

# Search
GET /api/users?search=john

# Multiple filters
GET /api/orders?status=pending&customer_id=123&date_from=2024-01-01

Rate Limiting

Response Headers

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1677234567
Retry-After: 3600

Implementation

// Express middleware
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: {
      code: 'RATE_LIMIT_EXCEEDED',
      message: 'Too many requests, please try again later.'
    }
  },
  standardHeaders: true,
  legacyHeaders: false,
});

app.use('/api/', limiter);

Caching

Cache Headers

# Response headers
Cache-Control: public, max-age=3600
ETag: "abc123"
Last-Modified: Wed, 21 Feb 2024 07:28:00 GMT

# Conditional request
If-None-Match: "abc123"
If-Modified-Since: Wed, 21 Feb 2024 07:28:00 GMT

# Response if unchanged
HTTP/1.1 304 Not Modified

Cache Strategies

// Express example
app.get('/api/public/config', (req, res) => {
  res.set('Cache-Control', 'public, max-age=86400'); // 24 hours
  res.json(config);
});

app.get('/api/users/:id', (req, res) => {
  res.set('Cache-Control', 'private, max-age=300'); // 5 minutes
  res.json(user);
});

app.get('/api/sensitive', (req, res) => {
  res.set('Cache-Control', 'no-store'); // Never cache
  res.json(sensitiveData);
});

Documentation

OpenAPI/Swagger Example

openapi: 3.0.0
info:
  title: My API
  version: 1.0.0
  description: API for managing users and posts

paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'
                  
    post:
      summary: Create user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreate'
      responses:
        '201':
          description: Created

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
        email:
          type: string
        name:
          type: string

Security Best Practices

Input Validation

const { body, validationResult } = require('express-validator');

app.post('/api/users',
  [
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 8 }),
    body('name').trim().escape()
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
    // Process request
  }
);

CORS Configuration

const cors = require('cors');

app.use(cors({
  origin: ['https://app.example.com'],
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
}));

Helmet for Security Headers

const helmet = require('helmet');

app.use(helmet());
// Sets various HTTP headers for security

Testing

// Jest example
describe('POST /api/users', () => {
  it('should create a new user', async () => {
    const response = await request(app)
      .post('/api/users')
      .send({
        email: 'test@example.com',
        name: 'Test User',
        password: 'password123'
      })
      .expect(201);
      
    expect(response.body).toHaveProperty('id');
    expect(response.body.email).toBe('test@example.com');
  });
  
  it('should return 400 for invalid email', async () => {
    await request(app)
      .post('/api/users')
      .send({
        email: 'invalid-email',
        name: 'Test User',
        password: 'password123'
      })
      .expect(400);
  });
});

Monitoring & Logging

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// Middleware
app.use((req, res, next) => {
  logger.info({
    method: req.method,
    url: req.url,
    ip: req.ip,
    userAgent: req.get('user-agent')
  });
  next();
});

Conclusion

Building a great API requires careful planning and attention to detail. Follow REST principles, use consistent naming conventions, provide clear error messages, and document thoroughly. Remember: your API is a product that developers will use—make their experience great!

M

Michael Rodriguez

Published on March 3, 2024

Share: