DEV Community

Alex Chen
Alex Chen

Posted on

Designing a REST API That Developers Actually Like Using

Designing a REST API That Developers Actually Like Using

API design is UX for developers. Make it delightful.

Core Principles

1. Consistent — Same patterns everywhere
2. Predictable — No surprises
3. Self-documenting — Names explain themselves
4. Versioned — Changes don't break clients
5. Secure — Auth, validation, rate limiting
Enter fullscreen mode Exit fullscreen mode

URL Design

// ✅ Good: Nouns, not verbs
GET    /api/users              // List users
GET    /api/users/123          // Get specific user
POST   /api/users              // Create user
PUT    /api/users/123          // Full update
PATCH  /api/users/123          // Partial update
DELETE /api/users/123          // Delete user

// ❌ Bad: Verbs in URLs
GET    /api/getUsers
POST   /api/createUser
POST   /api/deleteUser/123

// ✅ Good: Nested resources (1-2 levels max)
GET    /api/users/123/orders           // User's orders
GET    /api/users/123/orders/456       // Specific order

// ❌ Bad: Too deep
GET    /api/users/123/orders/456/items/789/reviews
Enter fullscreen mode Exit fullscreen mode

Request/Response Format

Standard Response Envelope

// Success response
{
  "data": { /* resource or array */ },
  "meta": {
    "page": 1,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "age", "message": "Must be a positive integer" }
    ]
  },
  "request_id": "req_abc123"
}
Enter fullscreen mode Exit fullscreen mode

Filtering, Sorting, Pagination

// Filter
GET /api/users?role=admin&active=true&created_after=2026-01-01

// Sort
GET /api/users?sort=created_at&order=desc
GET /api/users?sort=name,age&order=asc,desc  // Multi-sort

// Pagination
GET /api/users?page=2&per_page=20
// Or cursor-based (better for large datasets):
GET /api/users?cursor=eyJpZCI6MTIzfQ==&limit=20

// Field selection (reduce payload)
GET /api/users?fields=id,name,email
GET /api/users/123?fields=id,name,orders(count)
Enter fullscreen mode Exit fullscreen mode

Search

// Search endpoint
GET /api/search?q=nodejs&type=articles&tags=javascript,tutorial&page=1

// Response
{
  "data": [
    {
      "id": "art_1",
      "title": "Node.js Guide",
      "highlight": {
        "title": "<em>Node.js</em> Guide",  // Highlighted matches
        "excerpt": "Learn <em>Node.js</em> from scratch..."
      },
      "_score": 0.95,
      "_type": "article"
    }
  ],
  "meta": { "total": 42, "query": "nodejs", "took_ms": 12 }
}
Enter fullscreen mode Exit fullscreen mode

Authentication

// Bearer token (most common for APIs)
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

// API key (for server-to-server)
X-API-Key: ak_live_abc123

// Never send tokens in URL parameters!
// ❌ GET /api/users?token=eyJhbG...
// ✅ Use Authorization header always
Enter fullscreen mode Exit fullscreen mode

Rate Limiting

// Response headers (always include!)
HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1740000000
Retry-After: 60

// When rate limited:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1740000100
Retry-After: 45

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests. Retry after 45 seconds."
  }
}
Enter fullscreen mode Exit fullscreen mode

Idempotency & Safety

Method Safe Idempotent Meaning
GET ✅ ✅ Read only
HEAD ✅ ✅ Headers only
OPTIONS ✅ ✅ Capabilities
PUT ❌ ✅ Replace entirely
DELETE ❌ ✅ Remove (same result if called again)
POST ❌ ❌ Create new (different each time)
PATCH ❌ ❌ Partial update

Real Implementation Example

const express = require('express');
const rateLimit = require('express-rate-limit');
const helmet = require('helmet');

const app = express();

app.use(helmet());
app.use(express.json({ limit: '10kb' }));

// Rate limiting
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                  // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
}));

// Request ID middleware
app.use((req, res, next) => {
  req.id = crypto.randomUUID();
  res.setHeader('X-Request-ID', req.id);
  next();
});

// Routes
app.get('/api/v1/users', async (req, res) => {
  try {
    const { page = 1, per_page = 20, sort = 'created_at', order = 'desc', role } = req.query;

    const result = await User.findAll({
      where: role ? { role } : {},
      limit: parseInt(per_page),
      offset: (parseInt(page) - 1) * parseInt(per_page),
      order: [[sort, order.toUpperCase()]],
    });

    res.json({
      data: result.rows,
      meta: {
        page: parseInt(page),
        per_page: parseInt(per_page),
        total: result.count,
        total_pages: Math.ceil(result.count / per_page),
      }
    });
  } catch (err) {
    res.status(500).json({ error: { code: 'INTERNAL', message: err.message }, request_id: req.id });
  }
});

app.post('/api/v1/users', async (req, res) => {
  try {
    const { error, value } = userSchema.validate(req.body);
    if (error) {
      return res.status(422).json({
        error: { code: 'VALIDATION_ERROR', message: 'Invalid input', details: error.details },
        request_id: req.id
      });
    }

    const user = await User.create(value);
    res.status(201).location(`/api/v1/users/${user.id}`).json({ data: user });
  } catch (err) {
    if (err.name === 'SequelizeUniqueConstraintError') {
      return res.status(409).json({ error: { code: 'DUPLICATE', message: 'Email already exists' }, request_id: req.id });
    }
    res.status(500).json({ error: { code: 'INTERNAL', message: err.message }, request_id: req.id });
  }
});

// 404 handler
app.use('/api/', (_req, res) => {
  res.status(404).json({ error: { code: 'NOT_FOUND', message: 'Resource not found' } });
});
Enter fullscreen mode Exit fullscreen mode

Quick Reference

Rule Do Don't
URLs /api/users /api/getUsers
HTTP methods Correct semantics POST for everything
Status codes Accurate semantic codes Always 200
Errors Structured with details Plain text messages
Pagination page + per_page Return everything
Versioning /api/v1/ Change existing endpoints
Auth Header Authorization Query param token
Naming snake_case camelCase (in JSON)
Dates ISO 8601 Timestamps

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

Follow @armorbreak for more backend content.

Top comments (0)