REST API Design Principles — Naming, Status Codes, Versioning

Why REST API Design Matters

An API is the front door of a service. When visitors (clients) enter a building (server), clear signage makes it easy to find what they need. A well-designed API should be intuitive enough to use without reading the documentation.

REST (Representational State Transfer) is an architectural style leveraging HTTP. Its core principles are resource-centric design, HTTP method usage, and statelessness.

URI Naming Rules

Basic Principles

# Bad design — uses verbs, inconsistent
GET  /getUsers
POST /createNewUser
GET  /user/delete/123
POST /updateUserProfile

# Good design — nouns, plural, hierarchical
GET    /users              # List users
POST   /users              # Create user
GET    /users/123          # Get specific user
PUT    /users/123          # Full update of user
PATCH  /users/123          # Partial update of user
DELETE /users/123          # Delete user

Naming Checklist

RuleGood exampleBad example
Use plural nouns/users, /orders/user, /order
Use lowercase/user-profiles/UserProfiles
Use hyphens/blog-posts/blog_posts, /blogPosts
No verbsPOST /users/createUser
No file extensions/users/123/users/123.json
Express relationships/users/123/orders/getUserOrders?id=123

Relational Resources

# User's order list
GET /users/123/orders

# User's specific order
GET /users/123/orders/456

# Order's payment info
GET /users/123/orders/456/payment

# Avoid deep nesting (3+ levels)
# Bad: /users/123/orders/456/items/789/reviews
# Good: /order-items/789/reviews

HTTP Methods and Status Codes

Method Semantics

MethodPurposeIdempotentSafeRequest body
GETReadYesYesNone
POSTCreateNoNoYes
PUTFull replaceYesNoYes
PATCHPartial updateNoNoYes
DELETEDeleteYesNoNone/Optional

Status Code Guide

# Success responses
200 OK              # General success (GET, PUT, PATCH, DELETE)
201 Created          # Resource created successfully (POST)
204 No Content       # Success with no response body (DELETE)

# Client errors
400 Bad Request      # Request data validation failed
401 Unauthorized     # Authentication failed (no/expired token)
403 Forbidden        # Authorization failed (no permission)
404 Not Found        # Resource not found
409 Conflict         # Conflict (duplicate data, etc.)
422 Unprocessable    # Valid syntax but semantic error
429 Too Many Requests # Rate limit exceeded

# Server errors
500 Internal Error   # Internal server error
502 Bad Gateway      # Upstream server error
503 Service Unavailable # Service temporarily unavailable

Error Response Patterns

A consistent error response format greatly improves client developer productivity.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Input data is invalid.",
    "details": [
      {
        "field": "email",
        "message": "Not a valid email format.",
        "value": "invalid-email"
      },
      {
        "field": "age",
        "message": "Age must be 0 or greater.",
        "value": -5
      }
    ],
    "timestamp": "2026-02-19T14:30:00Z",
    "request_id": "req_abc123"
  }
}

Express.js Error Handling Implementation

// Error response helper class
class ApiError extends Error {
  constructor(statusCode, code, message, details = []) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;       // Error code (machine-readable)
    this.details = details; // Details array
  }
}

// Create validation error
function validationError(details) {
  return new ApiError(
    400,
    "VALIDATION_ERROR",
    "Input data is invalid.",
    details
  );
}

// Global error handler
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const response = {
    error: {
      code: err.code || "INTERNAL_ERROR",
      message: err.message || "An internal server error occurred.",
      details: err.details || [],
      timestamp: new Date().toISOString(),
      request_id: req.id  // Request tracking ID
    }
  };

  // Hide details for 500 errors (security)
  if (statusCode === 500) {
    console.error("Server error:", err);
    response.error.message = "An internal server error occurred.";
    response.error.details = [];
  }

  res.status(statusCode).json(response);
});

Pagination

# Cursor-based (recommended — suitable for large datasets)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20

# Response
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ",
    "has_more": true,
    "limit": 20
  }
}

# Offset-based (simple but degrades at scale)
GET /users?page=3&per_page=20

# Response
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}
ApproachProsCons
Cursor-basedStable at scale, real-time dataUnknown total pages, no random access
Offset-basedSimple implementation, random page accessDuplicates/misses on data changes, slow

Versioning

# Method 1: URL path (most intuitive, recommended)
GET /v1/users
GET /v2/users

# Method 2: Query parameter
GET /users?version=2

# Method 3: Header
GET /users
Accept: application/vnd.myapi.v2+json
MethodProsCons
URL pathClear, easy to cacheURL changes
Query parameterPreserves URL structureDefault value confusion
HeaderClean URLsDifficult to test, low discoverability
# Filtering — use query parameters
GET /users?status=active&role=admin

# Sorting — sort parameter
GET /users?sort=-created_at,name
# - prefix means descending, default is ascending

# Search — q or search parameter
GET /users?q=alice

# Field selection — fields parameter (optimize response size)
GET /users?fields=id,name,email

# Combined usage
GET /users?status=active&sort=-created_at&fields=id,name&limit=10

Practical Tips

  • Consistency is paramount: Unify naming, response format, and error structure across the entire project. An inconsistent API is difficult to use no matter how good the documentation is.
  • Choose pragmatism over HATEOAS: A developer-friendly API is better than a theoretically perfect REST API.
  • Apply rate limiting from the start: Include 429 responses and Retry-After headers. Adding them later makes it hard for existing clients to adapt.
  • Include a request ID in every response: This is essential debugging information that links client and server logs.
  • Distinguish between PUT and PATCH: PUT is a full replacement (missing fields become null), PATCH is a partial update (only sent fields change). PATCH is appropriate for most update requests.
  • Document with OpenAPI (Swagger): Auto-generating API specs from code prevents inconsistencies between documentation and the actual API.

Was this article helpful?