DEV Community

Cover image for RESTful API Design: Best Practices for Building Scalable APIs
Sepehr Mohseni
Sepehr Mohseni

Posted on

RESTful API Design: Best Practices for Building Scalable APIs

A well-designed API is the foundation of any successful application. This guide covers best practices for designing RESTful APIs that are intuitive, scalable, and maintainable.

Resource Naming Conventions

Use Nouns, Not Verbs

# Good - Resources are nouns
GET    /users              # List users
POST   /users              # Create user
GET    /users/123          # Get user
PUT    /users/123          # Update user
DELETE /users/123          # Delete user

# Bad - Verbs in URLs
GET    /getUsers
POST   /createUser
GET    /getUserById/123
Enter fullscreen mode Exit fullscreen mode

Hierarchical Resources

# Nested resources for relationships
GET    /users/123/orders           # User's orders
GET    /users/123/orders/456       # Specific order
POST   /users/123/orders           # Create order for user

# Alternative: Query parameters for filtering
GET    /orders?user_id=123         # Filter orders by user
Enter fullscreen mode Exit fullscreen mode


Use plural nouns for collections (/users) and singular identifiers for specific resources (/users/123).

Consistent Naming

# Use kebab-case for multi-word resources
GET /user-profiles
GET /order-items
GET /shipping-addresses

# Use snake_case for query parameters
GET /products?category_id=5&sort_by=price&sort_order=desc
Enter fullscreen mode Exit fullscreen mode

HTTP Methods and Status Codes

Proper Method Usage

GET     - Retrieve resources (idempotent, cacheable)
POST    - Create new resources
PUT     - Replace entire resource (idempotent)
PATCH   - Partial update (not idempotent)
DELETE  - Remove resource (idempotent)
OPTIONS - Get allowed methods
HEAD    - Get headers only
Enter fullscreen mode Exit fullscreen mode

Status Code Reference

// Success responses
200 OK              // Successful GET, PUT, PATCH
201 Created         // Successful POST (include Location header)
204 No Content      // Successful DELETE

// Client errors
400 Bad Request     // Invalid request body/parameters
401 Unauthorized    // Missing or invalid authentication
403 Forbidden       // Authenticated but not authorized
404 Not Found       // Resource doesn't exist
409 Conflict        // Resource conflict (duplicate, etc.)
422 Unprocessable   // Validation errors
429 Too Many Requests // Rate limit exceeded

// Server errors
500 Internal Error  // Unexpected server error
502 Bad Gateway     // Upstream service error
503 Service Unavailable // Temporary overload
Enter fullscreen mode Exit fullscreen mode

Request and Response Design

Consistent Response Structure

// Success response
{
  "data": {
    "id": 123,
    "name": "John Doe",
    "email": "john@example.com",
    "created_at": "2024-01-15T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

// Collection response with pagination
{
  "data": [
    { "id": 1, "name": "Product A" },
    { "id": 2, "name": "Product B" }
  ],
  "meta": {
    "total": 150,
    "page": 1,
    "per_page": 20,
    "total_pages": 8
  },
  "links": {
    "self": "/products?page=1",
    "next": "/products?page=2",
    "last": "/products?page=8"
  }
}

// Error response
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request contains invalid data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Must be at least 18"
      }
    ]
  },
  "meta": {
    "request_id": "req_xyz789"
  }
}
Enter fullscreen mode Exit fullscreen mode


Always include a request ID in responses for debugging and support purposes.

Filtering, Sorting, and Pagination

# Filtering
GET /products?category=electronics&brand=apple&min_price=100&max_price=500

# Sorting
GET /products?sort=price:asc,created_at:desc

# Pagination (offset-based)
GET /products?page=2&per_page=20

# Pagination (cursor-based - better for large datasets)
GET /products?cursor=eyJpZCI6MTAwfQ&limit=20

# Field selection (sparse fieldsets)
GET /users/123?fields=id,name,email

# Including related resources
GET /orders/123?include=user,items,shipping_address
Enter fullscreen mode Exit fullscreen mode

API Versioning

URL Path Versioning (Recommended)

GET /v1/users
GET /v2/users

# Clear and explicit
# Easy to route and cache
# Visible in logs and documentation
Enter fullscreen mode Exit fullscreen mode

Header Versioning

GET /users
Accept: application/vnd.myapi.v2+json

# Cleaner URLs
# More complex to implement
# Harder to test in browser
Enter fullscreen mode Exit fullscreen mode

Version Deprecation Strategy

// Response headers for deprecated versions
{
  "headers": {
    "Deprecation": "Sun, 01 Jan 2025 00:00:00 GMT",
    "Sunset": "Sun, 01 Jul 2025 00:00:00 GMT",
    "Link": "</v2/users>; rel=\"successor-version\""
  }
}
Enter fullscreen mode Exit fullscreen mode

Authentication and Authorization

JWT Authentication

// Request with Bearer token
GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

// Token structure
{
  "header": {
    "alg": "RS256",
    "typ": "JWT"
  },
  "payload": {
    "sub": "user_123",
    "email": "user@example.com",
    "roles": ["user", "admin"],
    "iat": 1704067200,
    "exp": 1704153600
  }
}
Enter fullscreen mode Exit fullscreen mode

API Key Authentication

// Header-based (recommended)
GET /api/data
X-API-Key: sk_live_abc123xyz

// Query parameter (less secure - visible in logs)
GET /api/data?api_key=sk_live_abc123xyz
Enter fullscreen mode Exit fullscreen mode

OAuth 2.0 Scopes

# Define granular permissions
read:users      - Read user data
write:users     - Create/update users
delete:users    - Delete users
read:orders     - Read order data
admin           - Full access

# Token with scopes
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "read:users read:orders"
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting

Implementation Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1704067200

# When rate limited
HTTP/1.1 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1704067200
Enter fullscreen mode Exit fullscreen mode

Rate Limit Strategies

// Sliding window implementation
const rateLimiter = {
  // Per-user limits
  authenticated: {
    requests: 1000,
    window: '1 hour'
  },
  // Per-IP limits for unauthenticated
  anonymous: {
    requests: 100,
    window: '1 hour'
  },
  // Per-endpoint limits
  endpoints: {
    'POST /auth/login': { requests: 5, window: '1 minute' },
    'POST /users': { requests: 10, window: '1 hour' }
  }
};
Enter fullscreen mode Exit fullscreen mode

Error Handling

Comprehensive Error Response

{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested user was not found",
    "details": {
      "resource_type": "User",
      "resource_id": "123"
    },
    "documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND",
    "request_id": "req_abc123"
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Code Catalog

const errorCodes = {
  // Authentication errors (1xxx)
  INVALID_CREDENTIALS: { status: 401, message: 'Invalid email or password' },
  TOKEN_EXPIRED: { status: 401, message: 'Authentication token has expired' },
  INSUFFICIENT_PERMISSIONS: { status: 403, message: 'You do not have permission' },

  // Validation errors (2xxx)
  VALIDATION_ERROR: { status: 422, message: 'Request validation failed' },
  INVALID_FORMAT: { status: 400, message: 'Invalid request format' },

  // Resource errors (3xxx)
  RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
  RESOURCE_CONFLICT: { status: 409, message: 'Resource already exists' },

  // Rate limiting (4xxx)
  RATE_LIMIT_EXCEEDED: { status: 429, message: 'Too many requests' },

  // Server errors (5xxx)
  INTERNAL_ERROR: { status: 500, message: 'An unexpected error occurred' },
  SERVICE_UNAVAILABLE: { status: 503, message: 'Service temporarily unavailable' },
};
Enter fullscreen mode Exit fullscreen mode

HATEOAS and Hypermedia

Self-Describing Responses

{
  "data": {
    "id": 123,
    "status": "pending",
    "total": 99.99
  },
  "links": {
    "self": { "href": "/orders/123" },
    "customer": { "href": "/users/456" },
    "items": { "href": "/orders/123/items" }
  },
  "actions": {
    "cancel": {
      "href": "/orders/123/cancel",
      "method": "POST",
      "title": "Cancel this order"
    },
    "pay": {
      "href": "/orders/123/pay",
      "method": "POST",
      "title": "Process payment"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

API Documentation

OpenAPI Specification

openapi: 3.0.3
info:
  title: My API
  version: 1.0.0
  description: A sample API

paths:
  /users:
    get:
      summary: List all users
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
      responses:
        '200':
          description: Successful response
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/User'

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: integer
        name:
          type: string
        email:
          type: string
          format: email
Enter fullscreen mode Exit fullscreen mode

Conclusion

Well-designed APIs are intuitive, consistent, and scalable. By following these best practices—proper resource naming, appropriate status codes, comprehensive error handling, and clear documentation—you create APIs that developers love to use.

Key takeaways:

  • Use nouns for resources, HTTP methods for actions
  • Return appropriate status codes
  • Implement proper versioning from the start
  • Design comprehensive error responses
  • Document everything with OpenAPI

Top comments (0)