DEV Community

Alex Chen
Alex Chen

Posted on

REST API Design: Building APIs Developers Love (2026)

REST API Design: Building APIs Developers Love (2026)

A great API is a pleasure to use. A terrible one drives developers away. Here's how to design APIs that people actually want to work with.

Core Principles

1. Consistency → Same pattern everywhere, no surprises
2. Simplicity → Easy to understand, minimal cognitive load
3. Predictability → Same input always produces same output
4. Self-documentation → Clear enough to use without external docs
5. Performance → Fast responses, efficient data transfer
6. Security → Authenticated, authorized, rate-limited
7. Evolvability → Versioned, backward-compatible changes
Enter fullscreen mode Exit fullscreen mode

URL Design & Resource Naming

# Use nouns (resources), not verbs:
# ❌ /getUsers /createUser /deleteUser
# ✅ GET    /users          List users
# ✅ POST   /users          Create user
# ✅ GET    /users/123      Get specific user
# ✅ PUT    /users/123      Replace user
# ✅ PATCH  /users/123      Partial update
# ✅ DELETE /users/123      Delete user

# Plural nouns for collections:
/users          # Collection of all users
/users/123      # Specific resource

# Nested resources (relationships):
/users/123/orders              # Orders belonging to user 123
/users/123/orders/456          # Specific order of specific user
# Don't nest too deep (max 2-3 levels):
# ✅ /users/123/orders
# ❌ /users/123/orders/456/items/789/reviews

# Query parameters for filtering/sorting/pagination:
GET /users?role=admin&status=active&sort=name&order=asc&page=1&limit=20
# Not path segments: /users/role/admin/status/active (too complex)

# Search as a resource action:
GET /users/search?q=john&fields=id,name,email

# Actions (when CRUD doesn't fit — use verbs sparingly):
POST /users/123/activate       # State change (not CRUD)
POST /orders/123/cancel        # Cancel an order
POST /password/reset           # Initiate password reset
Enter fullscreen mode Exit fullscreen mode

HTTP Methods & Status Codes

// HTTP Methods — use them correctly:

// GET — Retrieve data (safe, idempotent, cacheable)
app.get('/api/products', listProducts);
app.get('/api/products/:id', getProduct);

// POST — Create new resource (not idempotent)
app.post('/api/products', createProduct); // Returns 201 with Location header

// PUT — Full replacement (idempotent)
app.put('/api/products/:id', replaceProduct);

// PATCH — Partial update (idempotent if implemented correctly)
app.patch('/api/products/:id', updateProduct);

// DELETE — Remove resource (idempotent)
app.delete('/api/products/:id', deleteProduct);

// Status codes — be precise!

// 2xx Success:
res.status(200).json(data);        // OK (standard success)
res.status(201).json(createdData); // Created (include Location header)
res.status(204).send();            // No Content (successful delete)
res.status(202).json({ taskId });  // Accepted (async processing started)

// 3xx Redirection:
res.status(301).redirect('https://...'); // Permanent redirect
res.status(304).send();                 // Not Modified (ETag match)

// 4xx Client Errors:
res.status(400).json({ error: 'Bad Request', details: validationErrors });
res.status(401).json({ error: 'Unauthorized' }); // Missing or invalid auth
res.status(403).json({ error: 'Forbidden' });    // Authenticated but not allowed
res.status(404).json({ error: 'Not Found' });
res.status(409).json({ error: 'Conflict' });     // Duplicate resource
res.status(422).json({ error: 'Unprocessable Entity', fields: [...] });
res.status(429).json({ error: 'Too Many Requests', retryAfter: 60 });

// 5xx Server Errors:
res.status(500).json({ error: 'Internal Server Error' });
res.status(502).json({ error: 'Bad Gateway' });
res.status(503).json({ error: 'Service Unavailable' });
Enter fullscreen mode Exit fullscreen mode

Response Format & Pagination

// Standard response envelope (consistent structure!):

// Success response:
{
  "data": { /* the actual resource(s) */ },
  "meta": {
    "requestId": "req_abc123",
    "timestamp": "2026-06-05T10:30:00Z"
  }
}

// List response with pagination:
{
  "data": [
    { "id": 1, "name": "Product A" },
    { "id": 2, "name": "Product B" }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "totalItems": 150,
    "totalPages": 8,
    "hasNext": true,
    "hasPrev": false,
    "nextPageUrl": "/products?page=2&limit=20",
    "prevPageUrl": null
  },
  "meta": {
    "requestId": "req_def456"
  }
}

// Error response (ALWAYS consistent format!):
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "password", "message": "Must be at least 8 characters" }
    ],
    "incidentId": "inc_789xyz", // For support lookup!
    "documentationUrl": "https://docs.example.com/errors/VALIDATION_ERROR"
  }
}

// Pagination implementation:
async function listProducts(req, res) {
  const page = Math.max(1, parseInt(req.query.page) || 1);
  const limit = Math.min(100, Math.max(1, parseInt(req.query.limit) || 20));
  const offset = (page - 1) * limit;

  const [items, total] = await Promise.all([
    Product.find().skip(offset).limit(limit),
    Product.countDocuments()
  ]);

  res.json({
    data: items,
    pagination: {
      page,
      limit,
      totalItems: total,
      totalPages: Math.ceil(total / limit),
      hasNext: page * limit < total,
      hasPrev: page > 1,
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

Authentication & Rate Limiting

// API Key authentication (for server-to-server):
const apiAuth = async (req, res, next) => {
  const key = req.headers['x-api-key'];
  if (!key) return res.status(401).json({ error: 'API key required' });

  const apiKey = await ApiKey.findOne({ key, active: true })
    .populate('owner');

  if (!apiKey) return res.status(401).json({ error: 'Invalid API key' });

  req.apiKey = apiKey;
  req.user = apiKey.owner;
  next();
};

// JWT authentication (for user-facing APIs):
const jwtAuth = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Missing authorization header' });
  }

  try {
    const payload = jwt.verify(authHeader.slice(7), process.env.JWT_SECRET);
    req.user = await User.findById(payload.sub).select('-password');
    if (!req.user) return res.status(401).json({ error: 'User not found' });
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

// Rate limiting (per API key and per IP):
const rateLimiter = rateLimit({
  windowMs: 60 * 1000,     // 1 minute
  max: 100,                // 100 requests per minute per key/IP
  standardHeaders: true,
  legacyHeaders: false,
  keyGenerator: (req) => req.apiKey?.key || req.ip,
  handler: (req, res) => {
    res.status(429).json({
      error: 'Too many requests',
      retryAfter: Math.ceil(req.rateLimit.resetTime / 1000),
    });
  },
});

// Apply to all routes:
app.use('/api/', rateLimiter);
app.use('/api/public', publicRoutes); // Lower limits for unauthenticated
app.use('/api/', jwtAuth, authenticatedRoutes); // Higher limits for auth'd users
Enter fullscreen mode Exit fullscreen mode

Versioning Strategy

# URL versioning (most common, clearest):
/api/v1/users
/api/v2/users  # Different structure, breaking change

# Header versioning (cleaner URLs):
Accept: application/vnd.myapi.v2+json
GET /api/users

# Best practices:
# - Start with v1 from day one (even if you think you won't need it)
# - Support old versions for at least 6-12 months after releasing new one
# - Document deprecation timeline clearly
# - Never make breaking changes within a version

# When to bump versions:
# MAJOR: Remove or rename fields, change URL structure, new required params
# MINOR: Add optional fields, add new endpoints (backward compatible)
# PATCH: Bug fixes, documentation updates
Enter fullscreen mode Exit fullscreen mode

What's the best/worst API you've worked with? What made it great or terrible?

Follow @armorbreak for more practical developer guides.

Top comments (0)