Skip to main content

REST API Design Best Practices: Building APIs That Stand the Test of Time

Ryan Dahlberg
Ryan Dahlberg
September 15, 2025 15 min read
Share:
REST API Design Best Practices: Building APIs That Stand the Test of Time

TL;DR

Great REST APIs aren’t built by accident. They’re designed with intention, following proven principles that make them intuitive, scalable, and maintainable. This guide covers the essential best practices: resource-oriented design, proper HTTP method usage, thoughtful status codes, versioning strategies, pagination, filtering, error handling, security, and documentation. Whether you’re building your first API or refining an existing one, these patterns will help you create APIs that developers love to use.

Key Principles:

  • Design around resources, not actions
  • Use HTTP methods correctly (GET, POST, PUT, PATCH, DELETE)
  • Return appropriate status codes
  • Version your API from day one
  • Implement pagination and filtering
  • Provide clear, consistent error messages
  • Secure with authentication and rate limiting
  • Document thoroughly with examples

Why API Design Matters

I’ve integrated with hundreds of APIs over my career. Some were a joy to work with. Others made me want to throw my laptop out the window.

The difference? Thoughtful design.

A well-designed API feels intuitive. You can predict how endpoints work. Errors are clear. Documentation matches reality. You’re productive in minutes, not days.

A poorly-designed API? You’re constantly confused. Endpoints are inconsistent. Errors are cryptic. Documentation is outdated or missing. Every integration becomes a battle.

The stakes are high:

  • Your API is your product’s interface to the world
  • Poor design costs developers time and frustration
  • Good design enables adoption and growth
  • Redesigning APIs after launch is painful and expensive

Let’s build APIs that get it right the first time.


Principle 1: Think in Resources, Not Actions

REST is about resources, not procedures. This is the most fundamental principle.

Bad: Action-Oriented Design

POST /api/getUserById
POST /api/createNewUser
POST /api/updateUserEmail
POST /api/deleteUserAccount

This isn’t REST. This is RPC over HTTP.

Good: Resource-Oriented Design

GET    /api/users/123          # Get user
POST   /api/users              # Create user
PATCH  /api/users/123          # Update user
DELETE /api/users/123          # Delete user

Why this matters:

  • Resources are nouns (users, orders, products)
  • Operations are HTTP methods (GET, POST, PUT, DELETE)
  • URLs identify resources, methods specify actions
  • Consistent patterns across your entire API

Resource Naming Conventions

Use plural nouns:

✅ /api/users
✅ /api/orders
✅ /api/products

❌ /api/user
❌ /api/getUsers
❌ /api/user-list

Nested resources for relationships:

✅ /api/users/123/orders           # Orders for user 123
✅ /api/orders/456/items           # Items in order 456
✅ /api/teams/789/members          # Members of team 789

❌ /api/getUserOrders?userId=123
❌ /api/orderItems?orderId=456

Keep nesting shallow (2-3 levels max):

✅ /api/users/123/orders
✅ /api/teams/456/projects/789/tasks

❌ /api/companies/1/divisions/2/departments/3/teams/4/members/5/tasks

For deeply nested resources, provide direct access:

GET /api/tasks/999                # Direct access
GET /api/teams/4/tasks            # Team's tasks

Principle 2: Use HTTP Methods Correctly

HTTP methods aren’t suggestions. They have semantic meaning.

GET - Retrieve Resources

GET /api/users              # List all users
GET /api/users/123          # Get specific user
GET /api/users/123/orders   # Get user's orders

Characteristics:

  • Safe: No side effects, doesn’t modify server state
  • Idempotent: Multiple identical requests = same result
  • Cacheable: Responses can be cached
  • No request body: Parameters in query string

Query parameters for filtering:

GET /api/users?role=admin&status=active
GET /api/products?category=electronics&price_max=1000
GET /api/orders?created_after=2025-01-01&sort=created_at:desc

POST - Create Resources

POST /api/users
Content-Type: application/json

{
  "email": "user@example.com",
  "name": "John Doe",
  "role": "developer"
}

Response:

HTTP/1.1 201 Created
Location: /api/users/124
Content-Type: application/json

{
  "id": 124,
  "email": "user@example.com",
  "name": "John Doe",
  "role": "developer",
  "created_at": "2025-09-15T10:30:00Z"
}

Characteristics:

  • Not safe: Creates resources, modifies state
  • Not idempotent: Multiple POSTs create multiple resources
  • Return 201 Created with Location header
  • Include created resource in response body

PUT - Replace Resource

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

{
  "email": "newemail@example.com",
  "name": "John Doe",
  "role": "senior-developer"
}

Characteristics:

  • Not safe: Modifies state
  • Idempotent: Multiple identical PUTs = same result
  • Replaces entire resource: Must include all fields
  • Return 200 OK or 204 No Content

PATCH - Partial Update

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

{
  "role": "senior-developer"
}

Characteristics:

  • Not safe: Modifies state
  • Not idempotent: Depends on implementation
  • Updates specific fields: Only send what changes
  • Return 200 OK with updated resource

PUT vs PATCH:

  • PUT: Replace the entire resource (send all fields)
  • PATCH: Update specific fields (send only changes)

Use PATCH for most updates. It’s more flexible and efficient.

DELETE - Remove Resource

DELETE /api/users/123

Response:

HTTP/1.1 204 No Content

Or if returning confirmation:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "message": "User deleted successfully",
  "deleted_at": "2025-09-15T10:45:00Z"
}

Characteristics:

  • Not safe: Removes resources
  • Idempotent: Deleting same resource multiple times = same result
  • Return 204 No Content or 200 OK

Principle 3: Return Meaningful Status Codes

HTTP status codes communicate what happened. Use them correctly.

Success Codes (2xx)

200 OK - Request succeeded, returning data

GET /api/users/123        → 200 OK
PATCH /api/users/123      → 200 OK

201 Created - Resource created successfully

POST /api/users           → 201 Created
Location: /api/users/124

204 No Content - Success, no response body

DELETE /api/users/123     → 204 No Content
PUT /api/users/123        → 204 No Content

Client Error Codes (4xx)

400 Bad Request - Invalid request format or validation error

{
  "error": "validation_error",
  "message": "Invalid request data",
  "details": {
    "email": "Invalid email format",
    "age": "Must be a positive integer"
  }
}

401 Unauthorized - Missing or invalid authentication

{
  "error": "unauthorized",
  "message": "Authentication required",
  "hint": "Include a valid access token in the Authorization header"
}

403 Forbidden - Authenticated but not authorized

{
  "error": "forbidden",
  "message": "You don't have permission to delete this user",
  "required_role": "admin"
}

404 Not Found - Resource doesn’t exist

{
  "error": "not_found",
  "message": "User with ID 123 not found"
}

409 Conflict - Request conflicts with current state

{
  "error": "conflict",
  "message": "A user with email 'user@example.com' already exists",
  "conflicting_field": "email"
}

422 Unprocessable Entity - Validation failed on semantically correct request

{
  "error": "validation_error",
  "message": "Cannot create order with negative quantity",
  "details": {
    "quantity": "Must be greater than 0"
  }
}

429 Too Many Requests - Rate limit exceeded

{
  "error": "rate_limit_exceeded",
  "message": "Too many requests",
  "retry_after": 60,
  "limit": 100,
  "remaining": 0,
  "reset_at": "2025-09-15T11:00:00Z"
}

Server Error Codes (5xx)

500 Internal Server Error - Unexpected server error

{
  "error": "internal_error",
  "message": "An unexpected error occurred",
  "request_id": "req_abc123"
}

503 Service Unavailable - Server temporarily unavailable

{
  "error": "service_unavailable",
  "message": "Service temporarily unavailable for maintenance",
  "retry_after": 300
}

Key principle: Status codes are for machines (HTTP clients). Error messages are for humans (developers).


Principle 4: Version Your API from Day One

APIs evolve. Breaking changes happen. Versioning prevents breaking existing clients.

https://api.example.com/v1/users
https://api.example.com/v2/users

Pros:

  • Clear and explicit
  • Easy to route
  • Browser-friendly
  • Works with all clients

Cons:

  • URLs change between versions

This is the most common and recommended approach.

Header Versioning

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

Pros:

  • URLs don’t change
  • RESTful purists prefer it

Cons:

  • Less visible
  • Harder to test in browser
  • More complex routing

When to Create a New Version

Create a new major version when making breaking changes:

Breaking changes:

  • Removing endpoints
  • Removing fields from responses
  • Changing field types
  • Renaming fields or endpoints
  • Changing authentication mechanism
  • Changing behavior significantly

Non-breaking changes (don’t require new version):

  • Adding new endpoints
  • Adding new optional fields to requests
  • Adding new fields to responses
  • Adding new query parameters
  • Making required fields optional

Version support strategy:

v1: Deprecated, supported for 6 months
v2: Current stable version
v3: Beta, available for early adopters

Always provide a migration guide when releasing new versions.


Principle 5: Implement Pagination, Filtering, and Sorting

Large datasets need efficient querying.

Pagination

Offset-based pagination:

GET /api/users?limit=20&offset=40

Response:
{
  "data": [...],
  "pagination": {
    "total": 150,
    "limit": 20,
    "offset": 40,
    "has_more": true
  }
}

Cursor-based pagination (better for large datasets):

GET /api/users?limit=20&cursor=eyJ1c2VyX2lkIjoxMjN9

Response:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJ1c2VyX2lkIjoxNDN9",
    "has_more": true
  }
}

Why cursor-based is better:

  • Consistent results even with data changes
  • Better performance on large datasets
  • Prevents duplicate or missing items

Filtering

GET /api/users?role=admin&status=active
GET /api/products?category=electronics&price_min=100&price_max=500
GET /api/orders?created_after=2025-01-01&status=completed

Advanced filtering with operators:

GET /api/products?price[gte]=100&price[lte]=500
GET /api/users?created_at[gt]=2025-01-01
GET /api/orders?status[in]=pending,processing,shipped

Sorting

GET /api/users?sort=created_at:desc
GET /api/products?sort=price:asc,name:asc
GET /api/orders?sort=-created_at    # Minus = descending

Field Selection

Allow clients to request only needed fields:

GET /api/users?fields=id,name,email
GET /api/users/123?fields=id,name,orders(id,total)

Reduces payload size and improves performance.


Principle 6: Design Clear Error Responses

Errors are inevitable. Make them helpful.

Consistent Error Format

{
  "error": "validation_error",
  "message": "Request validation failed",
  "details": {
    "email": "Invalid email format",
    "password": "Must be at least 8 characters"
  },
  "request_id": "req_abc123",
  "timestamp": "2025-09-15T10:30:00Z"
}

Key elements:

  • error: Machine-readable error code
  • message: Human-readable description
  • details: Specific field-level errors
  • request_id: For support and debugging
  • timestamp: When error occurred

Validation Errors

POST /api/users
{
  "email": "invalid-email",
  "age": -5
}

Response:
HTTP/1.1 422 Unprocessable Entity
{
  "error": "validation_error",
  "message": "Validation failed",
  "details": {
    "email": "Must be a valid email address",
    "age": "Must be a positive integer"
  }
}

Business Logic Errors

POST /api/orders
{
  "product_id": 123,
  "quantity": 10
}

Response:
HTTP/1.1 422 Unprocessable Entity
{
  "error": "insufficient_inventory",
  "message": "Cannot create order: insufficient inventory",
  "details": {
    "product_id": 123,
    "requested_quantity": 10,
    "available_quantity": 3
  }
}

Authentication Errors

GET /api/users/123
Authorization: Bearer invalid_token

Response:
HTTP/1.1 401 Unauthorized
{
  "error": "invalid_token",
  "message": "Access token is invalid or expired",
  "hint": "Obtain a new token using the /auth/token endpoint"
}

Principle 7: Secure Your API

Security isn’t optional. Build it in from the start.

Authentication

Bearer tokens (recommended for APIs):

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

API keys (simple but less secure):

GET /api/users
X-API-Key: sk_live_abc123def456

OAuth 2.0 (for third-party access):

GET /api/users
Authorization: Bearer ya29.a0AfH6SMBx...

Rate Limiting

Prevent abuse and ensure fair usage:

Response Headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1694779200

When limit exceeded:

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

{
  "error": "rate_limit_exceeded",
  "message": "Rate limit exceeded",
  "limit": 100,
  "remaining": 0,
  "reset_at": "2025-09-15T11:00:00Z"
}

HTTPS Only

Always use HTTPS in production. No exceptions.

  • Encrypts data in transit
  • Prevents man-in-the-middle attacks
  • Required for OAuth and modern security standards

Input Validation

Validate and sanitize all inputs:

  • Check data types
  • Validate formats (email, URL, date)
  • Enforce length limits
  • Sanitize against injection attacks
  • Use allow-lists, not deny-lists

CORS Configuration

Configure Cross-Origin Resource Sharing correctly:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, PATCH, DELETE
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Max-Age: 86400

Principle 8: Document Thoroughly

Great APIs have great documentation.

What to Document

1. Authentication

  • How to obtain credentials
  • How to include credentials in requests
  • Token expiration and refresh

2. Endpoints

  • Resource URLs
  • Supported methods
  • Request parameters (path, query, body)
  • Response format and status codes
  • Examples (curl, JavaScript, Python)

3. Error Codes

  • All possible error codes
  • What they mean
  • How to resolve them

4. Rate Limits

  • Limits per endpoint
  • How to check remaining quota
  • What happens when exceeded

5. Versioning

  • Current version
  • Deprecated versions and sunset dates
  • Migration guides

OpenAPI/Swagger

Use OpenAPI specification for machine-readable docs:

openapi: 3.0.0
info:
  title: Example API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: '#/components/schemas/User'

Benefits:

  • Auto-generated documentation
  • Client SDK generation
  • API testing tools
  • Contract testing

Real-World Example: Well-Designed API

Here’s what a complete, well-designed API interaction looks like:

Create User

POST /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "email": "john@example.com",
  "name": "John Doe",
  "role": "developer"
}

Response:
HTTP/1.1 201 Created
Location: /api/v1/users/124
Content-Type: application/json
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87

{
  "id": 124,
  "email": "john@example.com",
  "name": "John Doe",
  "role": "developer",
  "created_at": "2025-09-15T10:30:00Z",
  "updated_at": "2025-09-15T10:30:00Z"
}

List Users with Filtering and Pagination

GET /api/v1/users?role=developer&status=active&limit=20&cursor=eyJ1c2VyX2lkIjoxMDB9
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 86

{
  "data": [
    {
      "id": 101,
      "email": "dev1@example.com",
      "name": "Developer One",
      "role": "developer",
      "status": "active",
      "created_at": "2025-09-10T08:00:00Z"
    },
    {
      "id": 102,
      "email": "dev2@example.com",
      "name": "Developer Two",
      "role": "developer",
      "status": "active",
      "created_at": "2025-09-11T09:00:00Z"
    }
  ],
  "pagination": {
    "next_cursor": "eyJ1c2VyX2lkIjoxMjB9",
    "has_more": true
  }
}

Update User

PATCH /api/v1/users/124
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "role": "senior-developer"
}

Response:
HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": 124,
  "email": "john@example.com",
  "name": "John Doe",
  "role": "senior-developer",
  "created_at": "2025-09-15T10:30:00Z",
  "updated_at": "2025-09-15T10:45:00Z"
}

Error Handling

POST /api/v1/users
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "email": "invalid-email",
  "name": ""
}

Response:
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json

{
  "error": "validation_error",
  "message": "Request validation failed",
  "details": {
    "email": "Must be a valid email address",
    "name": "Name is required and cannot be empty"
  },
  "request_id": "req_abc123",
  "timestamp": "2025-09-15T10:50:00Z"
}

Common API Design Mistakes

Learn from these common pitfalls:

1. Using GET for State-Changing Operations

❌ GET /api/users/123/delete
✅ DELETE /api/users/123

GET requests should be safe (no side effects).

2. Including Verbs in URLs

❌ POST /api/createUser
❌ GET /api/getUsers
✅ POST /api/users
✅ GET /api/users

The HTTP method is the verb. URLs should be nouns.

3. Inconsistent Naming

❌ /api/users
❌ /api/user-order
❌ /api/ProductCategories

✅ /api/users
✅ /api/user-orders
✅ /api/product-categories

Pick a convention (plural nouns, kebab-case) and stick to it.

4. Returning 200 for Errors

❌ HTTP 200 OK
{
  "success": false,
  "error": "User not found"
}

✅ HTTP 404 Not Found
{
  "error": "not_found",
  "message": "User not found"
}

Status codes exist for a reason. Use them.

5. Exposing Database Structure

❌ GET /api/users?where=status='active'&orderBy=created_at
✅ GET /api/users?status=active&sort=created_at:desc

Abstract your database. APIs should be stable even if your database schema changes.


Checklist: Is Your API Well-Designed?

Use this checklist to evaluate your API:

Resource Design:

  • URLs use nouns, not verbs
  • Resources are plural (/users, not /user)
  • Nested resources show relationships clearly
  • Naming is consistent across all endpoints

HTTP Methods:

  • GET for retrieval (safe, idempotent, cacheable)
  • POST for creation
  • PUT for full replacement
  • PATCH for partial updates
  • DELETE for removal
  • Methods match their semantic meaning

Status Codes:

  • 2xx for success
  • 4xx for client errors
  • 5xx for server errors
  • Specific codes used correctly (404, 422, etc.)

Versioning:

  • API is versioned (v1, v2)
  • Breaking changes require new version
  • Deprecation policy is clear

Query Features:

  • Pagination implemented (offset or cursor)
  • Filtering supported
  • Sorting supported
  • Field selection available

Error Handling:

  • Consistent error format
  • Machine-readable error codes
  • Human-readable messages
  • Field-level validation details
  • Request IDs for debugging

Security:

  • HTTPS only in production
  • Authentication required
  • Rate limiting implemented
  • Input validation on all endpoints
  • CORS configured correctly

Documentation:

  • All endpoints documented
  • Request/response examples provided
  • Error codes explained
  • Authentication instructions clear
  • OpenAPI/Swagger spec available

Conclusion

Great API design isn’t about following rules blindly. It’s about understanding principles and applying them thoughtfully.

The core principles:

  1. Think in resources - URLs are nouns, methods are verbs
  2. Use HTTP correctly - Methods, status codes, and headers have meaning
  3. Version early - Plan for evolution from day one
  4. Make it queryable - Pagination, filtering, sorting
  5. Handle errors well - Clear, consistent, actionable
  6. Secure by default - Authentication, rate limiting, HTTPS
  7. Document thoroughly - Your API is only as good as its docs

A well-designed API is:

  • Intuitive: Developers can predict how it works
  • Consistent: Patterns repeat across all endpoints
  • Robust: Handles errors gracefully
  • Scalable: Supports growth without breaking
  • Maintainable: Easy to extend and evolve

Build APIs that developers love.

Design with intention. Document with care. Test thoroughly. Listen to feedback.

Your API is your product’s handshake with the world. Make it a good one.


Resources:

“Good API design is invisible. Great API design is inevitable.”

#APIs #REST #API Design #Software Architecture #Best Practices #HTTP #Engineering