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)