RESTful API Design: Best Practices for Building Scalable APIs

Published: (January 4, 2026 at 08:41 AM EST)
6 min read
Source: Dev.to

Source: Dev.to

Resource Naming

✅ 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

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

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

Naming conventions

  • Kebab‑case for multi‑word resources

    GET /user-profiles
    GET /order-items
    GET /shipping-addresses
  • snake_case for query parameters

    GET /products?category_id=5&sort_by=price&sort_order=desc

HTTP Methods

MethodPurposeIdempotent?Cacheable?
GETRetrieve resources
POSTCreate new resources
PUTReplace an entire resource
PATCHPartial update
DELETERemove a resource
OPTIONSDiscover allowed methods
HEADRetrieve headers only

Status Codes

Success

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 Entity# Validation errors
429 Too Many Requests   # Rate limit exceeded

Server errors

500 Internal Server Error   # Unexpected server error
502 Bad Gateway              # Upstream service error
503 Service Unavailable      # Temporary overload

Response Payloads

Single‑resource success

{
  "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"
  }
}

Tip: Always include a request_id (or similar) in responses for debugging and support.

Query‑Based Features

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
  • Cursor‑based (better for large datasets)

    GET /products?cursor=eyJpZCI6MTAwfQ&limit=20

Sparse fieldsets (field selection)

GET /users/123?fields=id,name,email
GET /orders/123?include=user,items,shipping_address

Versioning

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

Deprecation headers (for older versions)

{
  "headers": {
    "Deprecation": "Sun, 01 Jan 2025 00:00:00 GMT",
    "Sunset": "Sun, 01 Jul 2025 00:00:00 GMT",
    "Link": "; rel=\"successor-version\""
  }
}

Authentication & Authorization

Bearer token (Authorization header)

GET /api/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT payload example

{
  "header": { "alg": "RS256", "typ": "JWT" },
  "payload": {
    "sub": "user_123",
    "email": "user@example.com",
    "roles": ["user", "admin"],
    "iat": 1704067200,
    "exp": 1704153600
  }
}

API‑Key (header – preferred)

GET /api/data
X-API-Key: sk_live_abc123xyz

API‑Key (query – less secure)

GET /api/data?api_key=sk_live_abc123xyz

Granular permission model

  • 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"
}

Rate Limiting

Successful response 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

Sliding‑window implementation (example)

const rateLimiter = {
  // Per‑user limits
  authenticated: {
    requests: 1000,
    window: '1 hour'
  },
  // Per‑IP limits for unauthenticated callers
  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' }
  }
};

Structured Error Catalog

{
  "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"
  }
}

Centralised error‑code map (example)

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: 'Insufficient permissions' },

  // Validation errors (2xxx)
  VALIDATION_ERROR: { status: 422, message: 'Validation failed' },

  // Resource errors (3xxx)
  RESOURCE_NOT_FOUND: { status: 404, message: 'Resource not found' },
  CONFLICT:           { status: 409, message: 'Resource conflict' },

  // Rate‑limit errors (4xxx)
  TOO_MANY_REQUESTS: { status: 429, message: 'Rate limit exceeded' },

  // Server errors (5xxx)
  INTERNAL_ERROR: { status: 500, message: 'Internal server error' }
};

Quick Checklist

  • Resources: nouns, plural for collections, singular IDs for items.
  • Naming: kebab‑case for URLs, snake_case for query params.
  • Methods: use the correct HTTP verb; keep them idempotent where appropriate.
  • Status codes: return the most specific code for each situation.
  • Responses: include data, meta (with request_id), and links when paginating.
  • Filtering / Sorting / Pagination: expose via query parameters.
  • Versioning: URL or media‑type versioning; deprecate with proper headers.
  • Auth: prefer header‑based tokens or API keys; embed scopes/roles.
  • Rate limiting: communicate limits via response headers; implement sliding windows.
  • Errors: consistent JSON structure, error codes, documentation links, and request IDs.

Follow these guidelines to build APIs that are clear, consistent, and easy to consume.

Error Definitions

RMISSIONS: { 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' },
};

Sample HAL‑style Response

{
  "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"
    }
  }
}

OpenAPI Specification (YAML)

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

API Design Best Practices

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.
Back to Blog

Related posts

Read more »

Stop Writing APIs Like It's 2015

We're in 2025, and many codebases still treat APIs as simple “endpoints that return JSON.” If your API design hasn’t evolved past basic CRUD routes, you’re sacr...