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 makes developers want to quit. Here's how to build the former.

Core Design Principles

1. Consistency — If /users returns {id, name}, then /posts should return {id, title}
   Same naming, same structure, same patterns everywhere

2. Predictability — Same input → Same output
   No magic behavior, no hidden side effects

3. Simplicity — Fewer endpoints doing more (right)
   vs many tiny endpoints that force N+1 requests

4. Self-documentation — The API should explain itself
   Good URLs, good error messages, good response structure

5. Performance by default — Pagination, filtering, field selection built in
   Don't make developers fetch everything to find one item
Enter fullscreen mode Exit fullscreen mode

URL Design

// Resources (nouns, not verbs!)
GET    /api/users              // List users
POST   /api/users              // Create user
GET    /api/users/123          // Get specific user
PUT    /api/users/123          // Full update
PATCH  /api/users/123          // Partial update
DELETE /api/users/123          // Delete user

// Nested resources (keep it shallow — max 2-3 levels)
GET    /api/users/123/posts           // User's posts
GET    /api/users/123/posts/456       // Specific post of specific user
// NOT: /api/users/123/posts/456/comments/789/replies (too deep!)

// Actions (when CRUD doesn't fit)
POST   /api/users/123/activate        // Activate account
POST   /api/posts/456/publish         // Publish draft
POST   /api/orders/567/cancel         // Cancel order

// Search & Filter
GET    /api/users?role=admin&status=active
GET    /api/posts?tag=javascript&sort=-created_at&page=1&limit=20

// ❌ Bad URL patterns:
/api/getUsers                          // Verbs in URL
/api/user-list                         // Inconsistent naming
/Users                                 // Capital letters (case-sensitive!)
/api/users/get/123                     // Unnecessary nesting
/v1/createUser                         // Verb as resource name
Enter fullscreen mode Exit fullscreen mode

Request/Response Format

// Standard response envelope:
{
  "data": [...],            // The actual data (array or object)
  "meta": {                // Metadata for pagination, etc.
    "page": 1,
    "limit": 20,
    "total": 150,
    "total_pages": 8
  },
  "links": {                // Navigation links (HATEOAS-lite)
    "self": "/api/users?page=1",
    "next": "/api/users?page=2",
    "prev": null
  }
}

// Single item response:
{
  "data": {
    "id": "usr_abc123",
    "name": "Alice",
    "email": "alice@example.com",
    "created_at": "2026-01-15T10:30:00Z",
    "updated_at": "2026-06-01T08:22:00Z"
  }
}

// Error response (ALWAYS consistent!):
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format",
        "code": "INVALID_EMAIL"
      },
      {
        "field": "password",
        "message": "Password must be at least 8 characters",
        "code": "PASSWORD_TOO_SHORT"
      }
    ],
    "request_id": "req_abc123"  // For support debugging!
  }
}

// HTTP Status Codes (use them correctly!)
200 OK                  // Success (GET, PATCH, PUT)
201 Created             // Resource created (POST)
204 No Content          // Success but no body (DELETE)
400 Bad Request         // Client error (validation, bad syntax)
401 Unauthorized        // Not authenticated (no token/invalid token)
403 Forbidden           // Authenticated but not allowed
404 Not Found           // Resource doesn't exist
409 Conflict           // State conflict (duplicate email, etc.)
422 Unprocessable      // Valid format but semantic error
429 Too Many Requests   // Rate limited
500 Internal Server Error // Something went wrong (log the details!)
502/503/504            // Upstream/gateway issues
Enter fullscreen mode Exit fullscreen mode

Query Parameters Convention

// Filtering:
GET /api/products?category=electronics&price_min=100&price_max=1000
// Multiple values for same field:
GET /api/products?color=red&color=blue     // AND logic (both red AND blue)
GET /api/products?status=pending,approved   // OR logic within field

// Sorting:
GET /api/users?sort=name                   // Ascending
GET /api/users?sort=-created_at            // Descending (- prefix)
GET /api/users?sort=name,-created_at       // Multi-sort

// Pagination:
GET /api/users?page=1&limit=20            // Offset-based
GET /api/users?cursor=abc123&limit=20     // Cursor-based (better for infinite scroll!)

// Field selection (reduce payload size):
GET /api/users?fields=id,name,email       // Only return these fields
GET /api/posts?fields=id,title,author.name  // Nested field selection!

// Search:
GET /api/users?q=alice                    // Full-text search
GET /api/products?search=wireless+mouse   // Alternative search param name

// Include related resources:
GET /api/posts/123?include=author,tags    // Eager load relations
Enter fullscreen mode Exit fullscreen mode

Implementation Example (Express.js)

import express from 'express';
import { z } from 'zod'; // Schema validation

const app = express();
app.use(express.json());

// Validation middleware factory
function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse({
      body: req.body,
      query: req.query,
      params: req.params,
    });

    if (!result.success) {
      return res.status(422).json({
        error: {
          code: 'VALIDATION_ERROR',
          message: 'Invalid request',
          details: result.error.issues.map(issue => ({
            field: issue.path.join('.'),
            message: issue.message,
          })),
          request_id: req.id,
        }
      });
    }

    Object.assign(req, result.data); // Validated data
    next();
  };
}

// Route definitions
const createUserSchema = z.object({
  body: z.object({
    email: z.string().email().max(254),
    password: z.string().min(8).max(128),
    name: z.string().min(1).max(100).trim(),
    role: z.enum(['user', 'creator']).optional().default('user'),
  }),
});

app.post('/api/users', validate(createUserSchema), async (req, res) => {
  try {
    const { email, password, name, role } = req.body;

    // Check duplicate
    const existing = await db.users.findByEmail(email);
    if (existing) {
      return res.status(409).json({
        error: { code: 'DUPLICATE_EMAIL', message: 'Email already registered', request_id: req.id }
      });
    }

    // Create user
    const user = await db.users.create({ email, password: await hash(password), name, role });

    res.status(201).json({ data: serialize(user) });
  } catch (err) {
    logger.error('Create user error', { err, requestId: req.id });
    res.status(500).json({
      error: { code: 'INTERNAL_ERROR', message: 'An unexpected error occurred', request_id: req.id }
    });
  }
});

// List endpoint with all features
app.get('/api/users', async (req, res) => {
  const { page = 1, limit = 20, sort = 'name', role, q } = req.query;

  const filter = {};
  if (role) filter.role = role;
  if (q) filter.search = q;

  const { items, total } = await db.users.find({
    ...filter,
    sort,
    page: Math.max(1, parseInt(page)),
    limit: Math.min(100, parseInt(limit)), // Cap at 100
  });

  res.json({
    data: items.map(serialize),
    meta: { page: parseInt(page), limit: parseInt(limit), total, total_pages: Math.ceil(total / limit) },
    links: {
      self: `/api/users?page=${page}&limit=${limit}`,
      next: total > page * limit ? `/api/users?page=${parseInt(page)+1}&limit=${limit}` : null,
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

API Versioning Strategy

// Option 1: URL Path Versioning (most common, clearest)
app.use('/v1/users', v1UserRoutes);
app.use('/v2/users', v2UserRoutes);
// Pros: Clear, cacheable per version, works with CDNs
// Cons: URL clutter

// Option 2: Header Versioning
app.use((req, res, next) => {
  const version = req.headers['api-version'] || 'v1';
  req.apiVersion = version;
  next();
});
// Pros: Clean URLs
// Cons: Harder to test in browser, not cache-friendly

// Option 3: Content Negotiation
app.get('/users', (req, res) => {
  const accept = req.headers.accept || '';
  if (accept.includes('vnd.api.v2+json')) {
    return v2Handler(req, res);
  }
  return v1Handler(req, res);
});
// Pros: RESTful purist approach
// Cons: Complex, confusing for beginners

// My recommendation: Start with URL path versioning.
// It's the simplest and most widely understood.
Enter fullscreen mode Exit fullscreen mode

What's your biggest API design pet peeve? What makes an API delightful to work with?

Follow @armorbreak for more practical developer guides.

Top comments (0)