RESTful API Design: Best Practices for Building Web APIs

Learn REST principles, resource naming, HTTP methods, status codes, and best practices. Design clean, maintainable, and scalable RESTful APIs.

published: reading time: 11 min read

RESTful API Design: Best Practices for Building Web APIs

REST (Representational State Transfer) is the dominant approach for designing web APIs. It provides a structured way to expose resources over HTTP. Understanding REST principles helps you build APIs that are intuitive, scalable, and maintainable.

The HTTP/HTTPS protocol post covers HTTP methods and status codes. This post focuses on applying those concepts to API design.


What is REST?

REST is an architectural style, not a specification. It was defined by Roy Fielding in 2000. The key idea: treat everything as a resource that clients can interact with through standard HTTP operations.

REST APIs expose resources through URLs. The HTTP method indicates the action:

graph LR
    A[Client] -->|GET /users| B[Read users]
    A -->|POST /users| C[Create user]
    A -->|PUT /users/123| D[Update user]
    A -->|DELETE /users/123| E[Delete user]

Resource Naming

Resource names are the foundation of REST API design. Good resource names are:

  • Nouns, not verbs - /users not /getUsers
  • Plural - /users not /user
  • Hierarchical - /users/123/orders for users orders
  • Consistent - Same pattern throughout
# Good resource naming
GET /users              # List users
GET /users/123           # Get user 123
GET /users/123/orders   # Get orders for user 123
POST /users             # Create user
PUT /users/123          # Update user 123
DELETE /users/123       # Delete user 123

# Bad naming (mixing verbs and nouns)
GET /getUsers
GET /getUserById/123
POST /createUser

Nested Resources

Nested resources express relationships:

# A user posts
GET /users/123/posts

# A post comments
GET /posts/456/comments

# Limit nesting depth - typically 2 levels is practical
GET /users/123/posts/789/comments  # Too deep

HTTP Methods in REST

REST uses HTTP methods to indicate actions.

GET

Retrieves resources. GET requests should be safe (no side effects) and idempotent.

GET /users
GET /users/123

POST

Creates new resources. Each POST request typically creates one resource.

POST /users
Content-Type: application/json

{"name": "Alice", "email": "alice@example.com"}

Response includes the created resource and a 201 status code:

HTTP/1.1 201 Created
Location: /users/124

PUT

Replaces a resource entirely. If you only need to update specific fields, use PATCH.

PUT /users/123
Content-Type: application/json

{"name": "Alice Smith", "email": "alice.smith@example.com"}

PATCH

Partially updates a resource. Only the specified fields change.

PATCH /users/123
Content-Type: application/json

{"email": "alice.new@example.com"}

DELETE

Removes a resource.

DELETE /users/123

Status Codes

REST APIs should use appropriate HTTP status codes:

Success Codes

CodeMeaningWhen to use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST creating resource
204No ContentSuccessful DELETE

Error Codes

CodeMeaningWhen to use
400Bad RequestMalformed request body
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but not authorized
404Not FoundResource does not exist
409ConflictResource conflict (duplicate, state conflict)
422Unprocessable EntityValidation errors
500Internal Server ErrorUnexpected server error
// Example: Successful response
{
  "id": 123,
  "name": "Alice",
  "email": "alice@example.com"
}

// Example: Error response
{
  "error": "validation_error",
  "message": "Email is required",
  "details": [
    { "field": "email", "message": "This field is required" }
  ]
}

Request and Response

Content Type

Always specify content type for requests with bodies:

POST /users
Content-Type: application/json

Pagination

For endpoints returning lists, use pagination:

GET /users?page=2&limit=20

Response includes pagination metadata:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 100,
    "totalPages": 5
  }
}

Filtering and Sorting

Support filtering and sorting through query parameters:

# Filter by status
GET /orders?status=pending

# Filter by multiple fields
GET /orders?status=shipped&customer_id=456

# Sort by field
GET /users?sort=created_at&order=desc

Field Selection

Let clients request specific fields:

GET /users?fields=id,name,email

API Versioning

APIs need to evolve without breaking existing clients. Versioning provides a way to introduce changes.

URL Path Versioning

# Most common approach
GET /v1/users
GET /v2/users

Header Versioning

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

Query Parameter Versioning

GET /users?version=2

URL path versioning is the most common because it is explicit and easy to test.

The API Versioning Strategies post covers this topic in depth.


Error Handling

Consistent error responses help clients handle failures:

// Standard error format
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The requested user does not exist",
    "details": {
      "resource": "user",
      "id": 999
    }
  }
}

Include enough information for debugging but do not leak sensitive details.

Handling Validation Errors

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "name", "message": "Name must be at least 2 characters" }
    ]
  }
}

Authentication

API Keys

Simple but limited. Suitable for server-to-server communication.

GET /users
X-API-Key: your-api-key-here

JWT (JSON Web Tokens)

Stateless tokens with embedded user information. Common for modern applications.

GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

OAuth 2.0

For APIs accessed by third-party applications. More complex but more powerful.

GET /user
Authorization: Bearer access_token_from_oauth

Rate Limiting

Protect your API from abuse by limiting request rates:

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

When clients exceed the limit:

HTTP/1.1 429 Too Many Requests
Retry-After: 3600

Best Practices Summary

RESTful API design comes down to a few core principles. Use nouns for resource names, not verbs. Use plural names consistently. Apply HTTP methods correctly and use the right status codes. Paginate list endpoints. Format errors consistently. Version your API from the start. Secure endpoints with appropriate authentication. Implement rate limiting. Document everything with OpenAPI. Make incremental changes carefully.


When to Use REST

REST is the right choice when:

  • You need a simple, well-understood API pattern
  • HTTP caching benefits your use case
  • You have multiple clients (web, mobile, third-party) consuming the same API
  • Stateless request-response fits your data access patterns
  • You want easy documentation and discoverability
  • Standard HTTP infrastructure (CDNs, proxies, caches) helps
  • You are building CRUD-oriented services

When Not to Use REST

REST may not be the best fit when:

  • Clients need different data shapes (overfetching/underfetching issues)
  • You need real-time updates (WebSockets, Server-Sent Events make more sense)
  • Your queries are highly complex and deeply nested (GraphQL may fit better)
  • You need batched operations that span multiple resources
  • Mobile apps with limited bandwidth need precise data fetching
  • Your API is tightly coupled to a single client needs

Production Failure Scenarios

FailureImpactMitigation
Missing paginationLarge datasets cause timeout or memory issuesImplement cursor or offset pagination; set reasonable limits
No rate limitingAPI can be overwhelmed; DoS vulnerabilityImplement rate limiting per client; return 429 with Retry-After
Inconsistent error responsesClients cannot handle errors gracefullyStandardize error format; document all error codes
Version not implementedBreaking changes affect existing clientsVersion from day one; support multiple versions during transition
N+1 query problemDatabase overwhelmed with queriesUse eager loading; batch related queries; optimize data fetching
Unbounded queriesComplex queries slow or crash databaseSet query complexity limits; implement query timeouts
Missing authenticationUnauthorized access to sensitive dataImplement authentication on all endpoints; verify permissions
Insufficient validationInvalid data corrupts databaseValidate all input; use schema validation; return 400 for bad data

Observability Checklist

Metrics

  • Request rate by endpoint, method, and client
  • Response time distribution (p50, p95, p99) per endpoint
  • Error rate by status code and endpoint
  • Active connections or concurrent requests
  • Authentication failures (attempted unauthorized access)
  • Rate limit hits (429 responses)
  • Payload size (request and response)
  • Cache hit ratio (if using caching)

Logs

  • All requests with request ID, method, path, status, duration
  • Authentication and authorization failures with attempted credentials
  • Validation errors with field-level details
  • Database query times for slow queries (> 100ms)
  • Rate limiting events with client identifier
  • Error stack traces with correlation IDs

Alerts

  • Error rate exceeds 1% for 5 minutes
  • p99 latency exceeds 2 seconds
  • Authentication failures spike (potential attack)
  • Rate limit hits exceed normal threshold
  • Unusual request size or pattern
  • Database query times increasing
  • Memory or CPU usage on API servers

Security Checklist

  • Implement authentication on every protected endpoint
  • Use authorization to verify permissions (not just authentication)
  • Validate and sanitize all input data
  • Use HTTPS for all API endpoints
  • Set appropriate timeouts for all requests
  • Implement rate limiting to prevent abuse
  • Log security events (auth failures, suspicious patterns)
  • Return minimal information in errors (do not leak internal details)
  • Use secure, HttpOnly, SameSite cookies for sessions
  • Implement CORS properly for cross-origin requests
  • Protect against injection attacks (SQL, NoSQL)
  • Validate Content-Type matches expected format
  • Implement HEAD and OPTIONS method security
  • Use API keys or JWT tokens, not passwords in URLs

Common Pitfalls / Anti-Patterns

Using Verbs in URLs

REST uses nouns for resources, not verbs for actions.

# WRONG - verbs in URLs
GET /api/getUsers
POST /api/createUser
POST /api/deleteUser

# CORRECT - nouns and HTTP methods
GET /api/users
POST /api/users
DELETE /api/users/123

Returning 200 for Errors

Always use appropriate status codes for errors.

# WRONG - 200 for error
HTTP/1.1 200 OK
{"error": "Not found"}

# CORRECT - proper status code
HTTP/1.1 404 Not Found
{"error": "User not found"}

Ignoring Pagination

Never return unbounded lists.

# WRONG - unbounded response
GET /api/users
# Could return millions of users

# CORRECT - paginated response
GET /api/users?page=1&limit=20
# Returns: {"data": [...], "pagination": {"page": 1, "limit": 20, "total": 1000}}

Not Implementing Versioning

APIs need to evolve without breaking clients.

# Version in URL path (most explicit)
GET /v1/users
GET /v2/users

# Version in header (cleaner URLs, harder to test)
GET /users
Accept: application/vnd.example.v2+json

Exposing Internal Details in Errors

Keep error responses informative but not leaky.

// WRONG - exposing internal details
{
  "error": "Database connection failed: mysql://user:pass@host:3306/db"
}

// CORRECT - safe error response
{
  "error": {
    "code": "INTERNAL_ERROR",
    "message": "An unexpected error occurred"
  }
}

Quick Recap

Key Bullets

  • REST uses nouns for resources (urls), not verbs (actions are HTTP methods)
  • Use plural names consistently: /users not /user
  • Use appropriate HTTP methods: GET (read), POST (create), PUT (replace), PATCH (modify), DELETE (remove)
  • Return proper status codes: 2xx for success, 4xx for client errors, 5xx for server errors
  • Implement pagination for list endpoints
  • Standardize error response format across the API
  • Version your API from the start to allow evolution
  • Secure endpoints with authentication and authorization
  • Implement rate limiting to protect against abuse
  • Validate all input and return 400 for invalid data

Copy/Paste Checklist

# Test REST endpoint with curl
curl -X GET "https://api.example.com/users?page=1&limit=10" \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  -H "Accept: application/json"

# Create resource with POST
curl -X POST "https://api.example.com/users" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice","email":"alice@example.com"}'

# Update resource with PATCH
curl -X PATCH "https://api.example.com/users/123" \
  -H "Content-Type: application/json" \
  -d '{"email":"alice.new@example.com"}'

# Delete resource
curl -X DELETE "https://api.example.com/users/123" \
  -H "Authorization: Bearer ..."

# Check API version (if using header versioning)
curl -X GET "https://api.example.com/users" \
  -H "Accept: application/vnd.example.v2+json"

Conclusion

RESTful API design comes down to applying HTTP conventions consistently. Resources are nouns, actions are HTTP methods, and responses use standard status codes. Good naming, proper error handling, and thoughtful versioning create APIs that developers actually want to use.

For comparing REST with GraphQL, see the GraphQL vs REST post. For API versioning details, see the API Versioning Strategies post.

Category

Related Posts

GraphQL vs REST: Choosing the Right API Paradigm

Compare GraphQL and REST APIs, understand when to use each approach, schema design, queries, mutations, and trade-offs between the two paradigms.

#api #graphql #rest

API Versioning Strategies: Managing API Evolution Without Breaking Clients

Learn different API versioning strategies including URL versioning, header versioning, and query parameters. Understand backward compatibility and deprecation practices.

#api #versioning #rest

Rate Limiting: Token Bucket, Sliding Window, and Distributed

Rate limiting protects APIs from abuse. Learn token bucket, sliding window, fixed window algorithms and distributed rate limiting at scale.

#api #rate-limiting #architecture