← Back to Blog

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

A well-designed API is like a well-designed building - it needs solid foundations, clear navigation, and room to grow. This guide covers resource naming, HTTP methods, status codes, versioning, pagination, error handling, and authentication patterns that will help you create APIs developers actually enjoy using.

Alex Kowalski
Alex KowalskiBackend Developer & API Architect

A well-designed API is like a well-designed building. It needs solid foundations, clear navigation, thoughtful organization, and room to grow. Just as an architect wouldn't start construction without blueprints, you shouldn't start coding endpoints without a clear design strategy.

Over the years, I've seen APIs that were a joy to integrate with and others that caused endless frustration. The difference almost always comes down to following established conventions and making intentional design decisions. This guide consolidates the patterns I've found most valuable.

Core Principles

Before diving into specifics, let's establish the foundational principles that guide good REST API design:

  1. Consistency: Similar operations should work similarly across all resources
  2. Predictability: Developers should be able to guess how things work
  3. Clarity: The API should be self-documenting where possible
  4. Flexibility: Support current needs without precluding future requirements
  5. Robustness: Handle edge cases gracefully

Every decision we make should serve these principles.

Resource Naming Conventions

The URI is the first thing developers see. It should communicate clearly what resource they're working with.

Use Nouns, Not Verbs

Resources are things, not actions. The HTTP method conveys the action.

Recommended:

GET    /articles
POST   /articles
GET    /articles/42
PUT    /articles/42
DELETE /articles/42

Avoid:

GET    /getArticles
POST   /createArticle
POST   /deleteArticle/42

The exception is for operations that don't map cleanly to CRUD. In these cases, a verb-based endpoint is acceptable:

POST   /articles/42/publish
POST   /users/123/reset-password

Use Plural Nouns

Consistency matters more than grammatical correctness. Using plurals for all collection endpoints eliminates ambiguity.

Recommended:

/users
/users/42
/users/42/orders
/users/42/orders/7

Avoid mixing:

/user        # Is this a single user or a collection?
/users/42

Use Kebab-Case for Multi-Word Resources

URIs are case-sensitive, but conventions help developers remember them.

Recommended:

/blog-posts
/order-items
/user-profiles

Avoid:

/blogPosts      # camelCase is for code, not URIs
/blog_posts     # Underscores are harder to read in URLs
/BlogPosts      # PascalCase creates case-sensitivity issues

Model Relationships Clearly

Nested resources should express ownership or strong relationships:

/users/42/orders           # Orders belonging to user 42
/orders/7/items            # Items within order 7
/organizations/5/members   # Members of organization 5

However, avoid deep nesting beyond two levels. If you find yourself at /users/42/orders/7/items/3/comments, consider flattening:

/order-items/3/comments    # If items have global IDs

Query Parameters for Filtering, Sorting, and Searching

Keep the URI focused on identifying resources. Use query parameters for everything else.

GET /articles?status=published
GET /articles?author=jane-doe
GET /articles?sort=-created_at
GET /articles?sort=title,-created_at
GET /articles?search=kubernetes
GET /articles?fields=id,title,summary

Conventions I recommend:

  • Filtering: Use the field name as the parameter: ?status=active
  • Sorting: Use sort with field names, prefix with - for descending: ?sort=-date,title
  • Field selection: Use fields for sparse fieldsets: ?fields=id,name,email
  • Search: Use search or q for full-text search: ?search=query

HTTP Methods

Each HTTP method has specific semantics. Using them correctly makes your API intuitive.

Method Reference

MethodPurposeIdempotentSafeRequest BodyResponse Body
GETRetrieve resource(s)YesYesNoYes
POSTCreate resourceNoNoYesYes
PUTReplace resource entirelyYesNoYesYes
PATCHPartially update resourceNo*NoYesYes
DELETERemove resourceYesNoNoOptional

*PATCH can be idempotent depending on implementation

GET: Retrieve Resources

GET requests should never modify state. They must be safe and idempotent.

GET /articles HTTP/1.1 Host: api.example.com Accept: application/json --- HTTP/1.1 200 OK Content-Type: application/json { "data": [ {"id": 1, "title": "First Article", "status": "published"}, {"id": 2, "title": "Second Article", "status": "draft"} ], "meta": { "total": 47, "page": 1, "per_page": 20 } }

POST: Create Resources

POST creates a new resource within a collection. The server assigns the identifier.

POST /articles HTTP/1.1 Host: api.example.com Content-Type: application/json { "title": "New Article", "content": "Article content here...", "author_id": 42 } --- HTTP/1.1 201 Created Location: /articles/48 Content-Type: application/json { "data": { "id": 48, "title": "New Article", "content": "Article content here...", "author_id": 42, "created_at": "2026-01-12T10:30:00Z" } }

Key points:

  • Return 201 Created on success
  • Include Location header with the new resource URI
  • Return the created resource with server-generated fields

PUT: Replace Resources

PUT replaces the entire resource. If fields are omitted, they should be cleared or set to defaults.

PUT /articles/48 HTTP/1.1 Host: api.example.com Content-Type: application/json { "title": "Updated Title", "content": "Completely new content...", "author_id": 42, "status": "published" } --- HTTP/1.1 200 OK Content-Type: application/json { "data": { "id": 48, "title": "Updated Title", "content": "Completely new content...", "author_id": 42, "status": "published", "updated_at": "2026-01-12T11:00:00Z" } }

PUT is idempotent: calling it multiple times with the same payload produces the same result.

PATCH: Partial Updates

PATCH updates only the specified fields. Unspecified fields remain unchanged.

PATCH /articles/48 HTTP/1.1 Host: api.example.com Content-Type: application/json { "status": "archived" } --- HTTP/1.1 200 OK Content-Type: application/json { "data": { "id": 48, "title": "Updated Title", "content": "Completely new content...", "author_id": 42, "status": "archived", "updated_at": "2026-01-12T11:30:00Z" } }

Consider using JSON Patch (RFC 6902) for complex updates:

PATCH /articles/48 HTTP/1.1 Content-Type: application/json-patch+json [ {"op": "replace", "path": "/title", "value": "New Title"}, {"op": "add", "path": "/tags/-", "value": "featured"} ]

DELETE: Remove Resources

DELETE removes a resource. It should be idempotent.

DELETE /articles/48 HTTP/1.1 Host: api.example.com --- HTTP/1.1 204 No Content

Important considerations:

  • Return 204 No Content on successful deletion
  • Return 404 Not Found if resource doesn't exist (debatable - some prefer 204)
  • Consider soft deletion for audit trails

HTTP Status Codes

Status codes communicate the result of an operation. Using them correctly eliminates the need for clients to parse response bodies to understand what happened.

Success Codes (2xx)

CodeMeaningWhen to Use
200OKSuccessful GET, PUT, PATCH, or DELETE
201CreatedSuccessful POST that created a resource
202AcceptedRequest accepted for async processing
204No ContentSuccess with no response body (DELETE)

Client Error Codes (4xx)

CodeMeaningWhen to Use
400Bad RequestMalformed request syntax, invalid data
401UnauthorizedMissing or invalid authentication
403ForbiddenValid auth but insufficient permissions
404Not FoundResource doesn't exist
405Method Not AllowedHTTP method not supported for resource
409ConflictRequest conflicts with current state
422Unprocessable EntitySyntactically correct but semantically invalid
429Too Many RequestsRate limit exceeded

Server Error Codes (5xx)

CodeMeaningWhen to Use
500Internal Server ErrorUnexpected server failure
502Bad GatewayInvalid response from upstream server
503Service UnavailableServer temporarily unavailable
504Gateway TimeoutUpstream server timeout

Choosing Between 400, 422, and 409

This is a common point of confusion:

  • 400 Bad Request: The request is malformed. JSON parsing failed, required headers missing, etc.
  • 422 Unprocessable Entity: The request is well-formed but contains semantic errors. Email format invalid, referenced resource doesn't exist, etc.
  • 409 Conflict: The request is valid but conflicts with current state. Duplicate unique constraint, optimistic lock failure, etc.

API Versioning

APIs evolve. Breaking changes are sometimes necessary. Versioning allows you to make changes without breaking existing clients.

Versioning Strategies

1. URI Path Versioning (Recommended for most cases)

GET /v1/articles
GET /v2/articles

Advantages:

  • Explicit and visible
  • Easy to route at load balancer level
  • Simple to document and test

Disadvantages:

  • Not "pure" REST (version isn't part of the resource)
  • Can lead to URI proliferation

2. Header Versioning

GET /articles HTTP/1.1 Accept: application/vnd.example.v2+json

Advantages:

  • Clean URIs
  • Follows content negotiation principles

Disadvantages:

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

3. Query Parameter Versioning

GET /articles?version=2

Advantages:

  • Easy to implement
  • Explicit in request

Disadvantages:

  • Optional parameter could be forgotten
  • Mixes metadata with resource parameters

Versioning Guidelines

  • Version the entire API, not individual endpoints
  • Support at least one previous version during transition
  • Clearly communicate deprecation timelines
  • Document breaking changes comprehensively
  • Consider using sunset headers:
HTTP/1.1 200 OK Sunset: Sat, 01 Jun 2026 00:00:00 GMT Deprecation: true Link: </v2/articles>; rel="successor-version"

Pagination

Any endpoint that returns collections must support pagination. Returning thousands of records in a single response is never acceptable.

Offset-Based Pagination

The simplest approach, using page and per_page (or limit and offset):

GET /articles?page=3&per_page=20 --- HTTP/1.1 200 OK Content-Type: application/json { "data": [...], "meta": { "current_page": 3, "per_page": 20, "total_pages": 12, "total_count": 234 }, "links": { "first": "/articles?page=1&per_page=20", "prev": "/articles?page=2&per_page=20", "next": "/articles?page=4&per_page=20", "last": "/articles?page=12&per_page=20" } }

Advantages:

  • Simple to understand and implement
  • Allows jumping to arbitrary pages

Disadvantages:

  • Performance degrades on large datasets
  • Inconsistent when data changes between requests

Cursor-Based Pagination

More efficient for large datasets and real-time data:

GET /articles?limit=20&cursor=eyJpZCI6MTAwfQ --- HTTP/1.1 200 OK Content-Type: application/json { "data": [...], "meta": { "has_more": true, "next_cursor": "eyJpZCI6MTIwfQ" }, "links": { "next": "/articles?limit=20&cursor=eyJpZCI6MTIwfQ" } }

The cursor is typically an encoded representation of the last item's sort keys.

Advantages:

  • Consistent performance regardless of dataset size
  • Stable results even when data changes

Disadvantages:

  • Cannot jump to arbitrary pages
  • More complex to implement

Pagination Best Practices

  • Set reasonable defaults (e.g., per_page=20)
  • Enforce maximum limits (e.g., per_page cannot exceed 100)
  • Always return pagination metadata
  • Include navigation links (following HATEOAS principles)
  • Document your pagination strategy

Error Handling

Good error responses help developers debug issues quickly. They should be consistent, informative, and secure.

Error Response Structure

{ "error": { "code": "VALIDATION_ERROR", "message": "The request contains invalid data", "details": [ { "field": "email", "code": "INVALID_FORMAT", "message": "Must be a valid email address" }, { "field": "age", "code": "OUT_OF_RANGE", "message": "Must be between 18 and 120" } ], "request_id": "req_abc123", "documentation_url": "https://docs.example.com/errors/VALIDATION_ERROR" } }

Key elements:

  • code: Machine-readable error identifier
  • message: Human-readable description
  • details: Specific field-level errors when applicable
  • request_id: Correlation ID for support and debugging
  • documentation_url: Link to more information

Error Handling Guidelines

Be specific but secure:

// Good - specific enough to fix { "error": { "code": "INVALID_CREDENTIALS", "message": "The email or password is incorrect" } } // Bad - reveals too much { "error": { "code": "USER_NOT_FOUND", "message": "No user exists with email [email protected]" } }

Use consistent error codes:

VALIDATION_ERROR
AUTHENTICATION_REQUIRED
PERMISSION_DENIED
RESOURCE_NOT_FOUND
CONFLICT
RATE_LIMIT_EXCEEDED
INTERNAL_ERROR

Include request context for debugging:

{ "error": { "code": "INTERNAL_ERROR", "message": "An unexpected error occurred", "request_id": "req_abc123", "timestamp": "2026-01-12T10:30:00Z" } }

Authentication

Security is non-negotiable. Choose an authentication strategy appropriate for your use case.

API Keys

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

GET /articles HTTP/1.1 X-API-Key: sk_live_abc123def456

Guidelines:

  • Use prefix conventions (sk_live_, sk_test_)
  • Transmit only over HTTPS
  • Implement key rotation
  • Log key usage for auditing

JWT (JSON Web Tokens)

Stateless authentication suitable for distributed systems.

GET /articles HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Token structure:

{ "header": { "alg": "RS256", "typ": "JWT" }, "payload": { "sub": "user_42", "iat": 1704974400, "exp": 1704978000, "scope": "articles:read articles:write" } }

Guidelines:

  • Use short expiration times (15-60 minutes)
  • Implement refresh token rotation
  • Use asymmetric signing (RS256) for distributed verification
  • Include only necessary claims

OAuth 2.0

The standard for delegated authorization.

POST /oauth/token HTTP/1.1 Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=abc123 &redirect_uri=https://app.example.com/callback &client_id=client_xyz &client_secret=secret_xyz

Use OAuth 2.0 when:

  • Third-party applications need access to user data
  • You need granular permission scopes
  • You require standardized flows

Authentication Best Practices

  • Always use HTTPS
  • Implement rate limiting on authentication endpoints
  • Use secure token storage (httpOnly cookies or secure storage)
  • Implement proper token revocation
  • Log authentication events
  • Consider implementing MFA for sensitive operations

Putting It All Together

Here's a complete example demonstrating these principles:

# List articles with filtering, sorting, and pagination GET /v1/articles?status=published&sort=-created_at&page=1&per_page=20 HTTP/1.1 Host: api.example.com Authorization: Bearer eyJhbGciOiJSUzI1NiIs... Accept: application/json --- HTTP/1.1 200 OK Content-Type: application/json X-Request-ID: req_abc123 X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 999 X-RateLimit-Reset: 1704978000 { "data": [ { "id": 47, "type": "article", "attributes": { "title": "REST API Best Practices", "status": "published", "created_at": "2026-01-12T10:00:00Z" }, "relationships": { "author": { "data": {"type": "user", "id": 42} } }, "links": { "self": "/v1/articles/47" } } ], "meta": { "current_page": 1, "per_page": 20, "total_pages": 3, "total_count": 47 }, "links": { "self": "/v1/articles?status=published&sort=-created_at&page=1&per_page=20", "next": "/v1/articles?status=published&sort=-created_at&page=2&per_page=20", "last": "/v1/articles?status=published&sort=-created_at&page=3&per_page=20" } }

Final Thoughts

Designing a good API is an investment that pays dividends. The time spent on thoughtful naming, consistent conventions, and comprehensive error handling will be repaid many times over in reduced support burden and happier developers.

Remember these key principles:

  • Be consistent in naming and behavior
  • Use HTTP semantics correctly
  • Provide clear, actionable error messages
  • Plan for evolution with versioning
  • Secure every endpoint appropriately
  • Document everything

An API is a contract with your users. Honor that contract by making it clear, stable, and well-documented. Your future self - and everyone who integrates with your API - will thank you.

Alex Kowalski
Written byAlex KowalskiBackend Developer & API Architect
Read more articles