APIs are contracts. Once published, they’re hard to change without breaking clients. Poor API design creates frustrated developers, increased support burden, and technical debt that compounds over time.

Good API design does the opposite. Intuitive APIs attract users. Consistent patterns reduce bugs. Clear documentation accelerates adoption.

This guide covers REST API best practices: resource naming, HTTP methods, versioning, error handling, pagination, and authentication patterns.

Core REST Principles

What REST Actually Means

REST stands for REpresentational State Transfer. Roy Fielding defined it in his 2000 doctoral dissertation as an architectural style with six constraints:1

  1. Client-Server: Separation of concerns between UI and data storage.
  2. Stateless: No session state stored on the server between requests.
  3. Cacheable: Responses must define themselves as cacheable or not.
  4. Uniform Interface: Consistent structure across the API.
  5. Layered System: Client can’t tell if connected directly to server.
  6. Code on Demand (optional): Server can extend client functionality.

Resources, Not Actions

REST is about resources (nouns), not actions (verbs). HTTP methods provide the verbs.

WrongRight
/getUsersGET /users
/createUserPOST /users
/deleteUser/123DELETE /users/123
/updateOrderPATCH /orders/123

The URL identifies the resource. The HTTP method specifies the action.

URL Structure and Naming

Use Nouns, Not Verbs

The HTTP method already provides the verb. Don’t repeat it in the URL.

BadGood
GET /getUsersGET /users
POST /createUserPOST /users
DELETE /deleteUser/123DELETE /users/123
PUT /updateOrderPUT /orders/123

Use Plural Nouns

Consistency matters more than the specific choice. Plurals are the convention.

/users          (collection)
/users/123      (specific user)
/users/123/orders (user's orders)

Using both /user and /users in the same API creates confusion.

Use Lowercase and Hyphens

Good: /user-profiles
Bad:  /UserProfiles
Bad:  /user_profiles

URLs are case-sensitive on some servers. Lowercase avoids ambiguity. Hyphens are more readable than underscores.

Hierarchy Represents Relationships

/users/123/orders/456/items/789

This URL structure communicates: Item 789 belongs to Order 456, which belongs to User 123.

Avoid Deep Nesting

More than three levels becomes unwieldy.

Too deep: /users/123/projects/456/tasks/789/comments/101

Better alternatives:
/task-comments/101
/tasks/789/comments/101
/comments/101?task_id=789

Query Parameters for Filtering

Use query parameters for filtering, sorting, and pagination—not path segments.

/users?status=active
/orders?created_after=2024-01-01
/products?category=electronics&sort=price

HTTP Methods (CRUD Mapping)

Standard Methods

MethodPurposeIdempotentSafe
GETRead resource(s)YesYes
POSTCreate resourceNoNo
PUTReplace resourceYesNo
PATCHUpdate resourceYesNo
DELETEDelete resourceYesNo

Idempotent: Multiple identical requests produce the same result. Safe: Request doesn’t modify server state.

GET - Read

Retrieve resources. Never modify state with GET requests.

GET /users           -> List all users
GET /users/123       -> Get user 123
GET /users?role=admin -> Filtered list

POST - Create

Create new resources. The server assigns the ID.

POST /users
Body: { "name": "John", "email": "john@example.com" }

Response: 201 Created
Location: /users/124
Body: { "id": 124, "name": "John", "email": "john@example.com" }

PUT - Full Replace

Replace the entire resource. Client sends complete representation.

PUT /users/123
Body: { "name": "John", "email": "new@example.com" }

If a field is omitted, it’s removed (or set to default). PUT is idempotent—calling it multiple times produces the same result.

PATCH - Partial Update

Update only specified fields. Other fields remain unchanged.

PATCH /users/123
Body: { "email": "new@example.com" }

Only the email changes. Name and other fields stay the same.

DELETE - Remove

Delete a resource.

DELETE /users/123

Response: 204 No Content

DELETE is idempotent. Deleting an already-deleted resource returns 404, but the end state is the same.

HTTP Status Codes

Use appropriate status codes. They communicate success, failure, and failure reasons.

Success Codes (2xx)

CodeMeaningUse Case
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST (resource created)
204No ContentSuccessful DELETE (no body returned)

Client Error Codes (4xx)

CodeMeaningUse Case
400Bad RequestMalformed request syntax
401UnauthorizedAuthentication required
403ForbiddenAuthenticated but not authorized
404Not FoundResource doesn’t exist
409ConflictState conflict (duplicate, version mismatch)
422Unprocessable EntityValidation failed
429Too Many RequestsRate limit exceeded

Server Error Codes (5xx)

CodeMeaningUse Case
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service error
503Service UnavailableMaintenance or overload

Choosing the Right Code

Be specific. Use 422 for validation errors, 400 for malformed requests. Use 401 for “who are you?” and 403 for “I know who you are, but you can’t do this.”

Never use 200 for errors. The status code should reflect the actual result.

Versioning Strategies

APIs evolve. Breaking changes happen. Versioning lets you evolve while giving clients time to migrate.

Why Version?

  • APIs change over time
  • Breaking changes require migration periods
  • Multiple versions may need to coexist
  • Clients shouldn’t break without warning

URI Versioning (Most Common)

/v1/users
/v2/users

Pros: Obvious, cacheable, easy to route. Cons: Changes URI structure, can feel inelegant.

This is the most widely adopted approach for good reason—it’s simple and explicit.

Header Versioning

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

Pros: Clean URIs, content negotiation. Cons: Hidden from casual inspection, harder to test in browser.

Query Parameter Versioning

/users?version=2

Pros: Simple, explicit. Cons: Caching complications, mixing resource identification with versioning.

Recommendation

Use URI versioning (/v1/) for simplicity and visibility. It’s the most commonly expected pattern.2

Version Lifecycle

  1. Release v2 while maintaining v1
  2. Deprecation notice (6-12 months warning)
  3. Sunset v1 with documented date
  4. Document migration path clearly

Error Handling

Consistent Error Format

Use the same error structure across your entire API.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request was invalid",
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address"
      }
    ]
  }
}

Error Response Best Practices

  • Use appropriate HTTP status code: The code communicates the category.
  • Include machine-readable error code: For programmatic handling.
  • Include human-readable message: For debugging and display.
  • Add field-level details for validation: Help users fix specific issues.
  • Include request ID: For debugging and support.
  • Never expose stack traces in production: Security risk.

Example Error Responses

Validation Error (422):

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      { "field": "email", "message": "Invalid email format" },
      { "field": "password", "message": "Minimum 8 characters required" }
    ],
    "request_id": "req_abc123"
  }
}

Not Found (404):

{
  "error": {
    "code": "NOT_FOUND",
    "message": "User with ID 123 not found",
    "request_id": "req_def456"
  }
}

Rate Limited (429):

{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests",
    "retry_after": 60,
    "request_id": "req_ghi789"
  }
}

Pagination

Never return unbounded lists. Pagination protects your server and improves client performance.

Offset-Based Pagination

GET /users?offset=20&limit=10

Response:
{
  "data": [...],
  "pagination": {
    "offset": 20,
    "limit": 10,
    "total": 153
  }
}

Pros: Simple to implement, allows random access (jump to page 5). Cons: Inconsistent with real-time data (items shift as data changes), slow at large offsets.

Cursor-Based Pagination

GET /users?cursor=eyJpZCI6MTIzfQ&limit=10

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

Pros: Consistent results, performant at any scale. Cons: No random access (can only go forward/back), more complex to implement.

Include Pagination Metadata

{
  "data": [...],
  "pagination": {
    "page": 2,
    "per_page": 10,
    "total": 153,
    "total_pages": 16
  },
  "links": {
    "self": "/users?page=2",
    "first": "/users?page=1",
    "prev": "/users?page=1",
    "next": "/users?page=3",
    "last": "/users?page=16"
  }
}

HATEOAS-style links help clients navigate without hardcoding URLs.

Filtering, Sorting, and Searching

Filtering with Query Parameters

GET /users?status=active
GET /orders?created_after=2024-01-01&created_before=2024-12-31
GET /products?price_min=10&price_max=100

Use descriptive parameter names. created_after is clearer than ca.

Sorting

GET /users?sort=created_at           (ascending)
GET /users?sort=-created_at          (descending, minus prefix)
GET /products?sort=price,-rating     (multiple fields)

The minus prefix for descending is a common convention.

Searching

GET /users?search=john
GET /products?q=wireless+headphones

Field Selection (Sparse Fieldsets)

Allow clients to request only needed fields.

GET /users?fields=id,name,email
GET /orders?fields=id,total,status

Reduces bandwidth and improves performance for mobile clients.

Authentication Patterns

API Keys

GET /users
X-API-Key: sk_live_abc123

Use for: Server-to-server communication, simple integrations. Security: Keep secret, rotate regularly.

Bearer Tokens (JWT)

GET /users
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Use for: User authentication, OAuth flows. Security: Tokens expire, include only necessary claims.

OAuth 2.0

Industry standard for delegated authorization. Multiple grant types for different scenarios (authorization code, client credentials, etc.).

Use for: Third-party access, user-authorized integrations.

Best Practices

  • Use HTTPS always: Never transmit credentials over unencrypted connections.
  • Rotate keys regularly: Automated rotation prevents accumulating risk.
  • Implement rate limiting: Protect against abuse.
  • Log authentication failures: Detect attacks.
  • Expire tokens appropriately: Balance security with user experience.

Request/Response Best Practices

Request Bodies

  • Use JSON (Content-Type: application/json)
  • Be consistent with naming (snake_case or camelCase, not both)
  • Validate and sanitize all input

Response Envelope

Consider wrapping responses consistently:

{
  "data": { ... },
  "meta": {
    "request_id": "req_abc123",
    "timestamp": "2024-01-15T10:30:00Z"
  }
}

Date Formats

Use ISO 8601 for all dates and times:

{
  "created_at": "2024-01-15T10:30:00Z",
  "expires_at": "2024-01-22T10:30:00Z"
}

Include timezone (Z for UTC or explicit offset).

Include links to related resources:

{
  "id": 123,
  "name": "John",
  "links": {
    "self": "/users/123",
    "orders": "/users/123/orders",
    "profile": "/users/123/profile"
  }
}

Clients can follow links rather than constructing URLs.

Documentation with OpenAPI

Why OpenAPI?

OpenAPI (formerly Swagger) is the industry standard specification for REST APIs.3

  • Auto-generates documentation
  • Enables client code generation
  • Integrates with testing tools
  • Creates interactive API explorers

Basic OpenAPI Structure

openapi: 3.0.0
info:
  title: My API
  version: 1.0.0
paths:
  /users:
    get:
      summary: List users
      responses:
        '200':
          description: Success

Documentation Tools

  • Swagger UI: Interactive API documentation
  • Redoc: Clean, readable documentation
  • Stoplight: Full API design platform

Common Mistakes to Avoid

  1. Using verbs in URLs: Let HTTP methods be the verbs.
  2. Inconsistent naming: Pick a convention, enforce it everywhere.
  3. Not versioning: You’ll need it eventually; plan from the start.
  4. Poor error messages: Help developers debug; don’t just say “Error.”
  5. Missing pagination: Unbounded lists kill servers and clients.
  6. No rate limiting: Protect your infrastructure from abuse.
  7. Ignoring HTTP semantics: Use proper status codes and methods.
  8. Breaking changes without versioning: Breaks client applications.

API Design Checklist

Before Building

  • Define resources and relationships
  • Choose naming conventions (plural nouns, lowercase, hyphens)
  • Plan versioning strategy
  • Design error response format
  • Decide pagination approach

During Building

  • Use proper HTTP methods for each operation
  • Return appropriate status codes
  • Implement consistent error handling
  • Add pagination to all list endpoints
  • Document with OpenAPI

Before Launch

  • Implement rate limiting
  • Set up authentication
  • Configure monitoring and logging
  • Write developer documentation
  • Create example code or SDKs

Conclusion

Good API design is about consistency, clarity, and respect for HTTP conventions. Follow established patterns. Be predictable. Make life easy for developers consuming your API.

The investment in thoughtful design pays dividends. APIs with clear contracts attract more users, generate fewer support requests, and evolve more gracefully over time.

Start with the basics: nouns for resources, proper HTTP methods, meaningful status codes, consistent error handling. Build from there.

Further Reading


References

Footnotes

  1. Fielding, Roy Thomas. “Architectural Styles and the Design of Network-based Software Architectures.” Doctoral dissertation, University of California, Irvine, 2000. https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm

  2. Microsoft. “Web API design best practices.” https://learn.microsoft.com/en-us/azure/architecture/best-practices/api-design

  3. OpenAPI Initiative. “OpenAPI Specification.” https://spec.openapis.org/oas/latest.html