DEV Community

Atlas Whoff
Atlas Whoff

Posted on

API Design Best Practices: RESTful Patterns That Scale

API Design Best Practices: RESTful Patterns That Scale

A poorly designed API becomes technical debt the moment the first client integrates with it. These patterns keep APIs maintainable as they grow.

Resource Naming

# Good — nouns, not verbs
GET    /users          # List users
GET    /users/:id      # Get user
POST   /users          # Create user
PUT    /users/:id      # Replace user
PATCH  /users/:id      # Update user fields
DELETE /users/:id      # Delete user

# Bad — verbs in URLs
POST   /createUser
GET    /getUser/:id
POST   /deleteUser/:id
Enter fullscreen mode Exit fullscreen mode

Nested Resources

# Posts belonging to a user
GET    /users/:userId/posts
POST   /users/:userId/posts
GET    /users/:userId/posts/:postId

# Limit nesting to 2 levels
# Deep nesting: /users/:id/posts/:id/comments/:id/likes — too deep
# Better: /comments/:id/likes
Enter fullscreen mode Exit fullscreen mode

Consistent Response Shape

// Every endpoint returns the same envelope
interface ApiResponse<T> {
  data: T | null;
  error: { code: string; message: string } | null;
  meta?: { total?: number; page?: number; limit?: number };
}

// Success
res.json({ data: user, error: null });

// Error
res.status(400).json({ data: null, error: { code: 'VALIDATION_ERROR', message: 'Email is required' } });

// List with pagination
res.json({ data: users, error: null, meta: { total: 150, page: 2, limit: 20 } });
Enter fullscreen mode Exit fullscreen mode

HTTP Status Codes

200 OK           — Success (GET, PUT, PATCH)
201 Created      — Resource created (POST)
204 No Content   — Success with no body (DELETE)
400 Bad Request  — Client validation error
401 Unauthorized — Not authenticated
403 Forbidden    — Authenticated but not authorized
404 Not Found    — Resource doesn't exist
409 Conflict     — Duplicate resource
422 Unprocessable — Valid JSON but invalid data
429 Too Many Requests — Rate limited
500 Internal Error — Server bug
Enter fullscreen mode Exit fullscreen mode

Filtering, Sorting, Pagination

// Query params for filtering/sorting
// GET /posts?status=published&sort=createdAt&order=desc&page=2&limit=20

app.get('/posts', async (req, res) => {
  const { status, sort = 'createdAt', order = 'desc', page = 1, limit = 20 } = req.query;

  const posts = await prisma.post.findMany({
    where: status ? { status: status as string } : undefined,
    orderBy: { [sort as string]: order },
    skip: (Number(page) - 1) * Number(limit),
    take: Number(limit),
  });

  const total = await prisma.post.count({ where: { status: status as string } });

  res.json({
    data: posts,
    error: null,
    meta: { total, page: Number(page), limit: Number(limit) },
  });
});
Enter fullscreen mode Exit fullscreen mode

Versioning

# URL versioning (most common)
/api/v1/users
/api/v2/users

# Header versioning
Accept: application/vnd.api+json;version=2
Enter fullscreen mode Exit fullscreen mode

Idempotency Keys

// Prevent duplicate operations on retry
app.post('/payments', async (req, res) => {
  const idempotencyKey = req.headers['idempotency-key'] as string;

  if (idempotencyKey) {
    const existing = await redis.get(`idempotency:${idempotencyKey}`);
    if (existing) return res.json(JSON.parse(existing));
  }

  const result = await processPayment(req.body);

  if (idempotencyKey) {
    await redis.setex(`idempotency:${idempotencyKey}`, 86400, JSON.stringify(result));
  }

  res.json(result);
});
Enter fullscreen mode Exit fullscreen mode

API scaffolding ships in the Ship Fast Skill Pack/api skill generates RESTful endpoints with proper error handling, pagination, and Zod validation. $49 at whoffagents.com.

Top comments (0)