env.dev

REST API Best Practices: Design Guide

Design better REST APIs with best practices for naming, versioning, pagination, error handling, and HATEOAS.

Last updated:

A well-designed REST API is predictable, consistent, and easy to consume. This guide covers the conventions and patterns that separate production-grade APIs from ad-hoc endpoints — including URI design, versioning, pagination, error handling, and authentication strategies. These practices are drawn from real-world API standards at Stripe, GitHub, and Google Cloud.

How Should You Structure Resource URIs?

URIs should identify resources (nouns), not actions (verbs). Use plural nouns for collections and nest resources only when there is a true parent-child relationship. Keep nesting shallow — two levels maximum.

PatternExampleNotes
Collection/usersPlural noun, lowercase
Single resource/users/42Identifier in the path
Nested resource/users/42/ordersMax 2 levels of nesting
Filtering/orders?status=pendingQuery params for filters
Avoid/getUser, /createOrderVerbs belong in HTTP methods
HTTP methods mapped to CRUD
GET    /articles          → List articles (collection)
GET    /articles/7        → Get article 7 (single resource)
POST   /articles          → Create a new article
PUT    /articles/7        → Replace article 7 entirely
PATCH  /articles/7        → Partially update article 7
DELETE /articles/7        → Delete article 7

What Are the Best API Versioning Strategies?

Version your API from day one. The three common approaches are URI path versioning, header versioning, and query parameter versioning. URI path versioning is the most widely used because it is visible and easy to route.

Versioning approaches
# URI path (most common — used by Stripe, GitHub)
GET /v1/users/42
GET /v2/users/42

# Custom header (used by GitHub as alternative)
GET /users/42
Accept: application/vnd.myapi.v2+json

# Query parameter
GET /users/42?version=2

Tip: Only increment the major version when you introduce breaking changes (removed fields, renamed endpoints, changed response shapes). Additive changes (new fields, new endpoints) are backward-compatible and do not require a new version.

Cursor vs Offset Pagination — Which Should You Use?

Offset-based pagination is simpler but breaks when data changes between requests. Cursor-based pagination is stable and performs better on large datasets because the database can use an index seek instead of counting rows.

Offset-based pagination
// Request
// GET /articles?page=3&per_page=20

// Response
{
  "data": [ /* 20 articles */ ],
  "meta": {
    "page": 3,
    "per_page": 20,
    "total": 542,
    "total_pages": 28
  }
}
// Problem: if a new article is inserted while the client
// is paginating, items shift and duplicates appear.
Cursor-based pagination (recommended)
// Request
// GET /articles?limit=20&after=eyJpZCI6MTAwfQ

// Response
{
  "data": [ /* 20 articles */ ],
  "meta": {
    "has_next": true,
    "next_cursor": "eyJpZCI6MTIwfQ",
    "has_previous": true,
    "previous_cursor": "eyJpZCI6MTAxfQ"
  }
}
// Cursor is an opaque, base64-encoded token (e.g. the last seen ID).
// Stable even when new rows are inserted or deleted.

How Should You Handle Errors Consistently?

Return a consistent error envelope with a machine-readable code, human-readable message, and optional field-level details. Always use appropriate HTTP status codes — do not return 200 for errors.

Consistent error response format
// 422 Unprocessable Entity
{
  "error": {
    "code": "validation_error",
    "message": "Request body failed validation",
    "details": [
      { "field": "email", "message": "must be a valid email address" },
      { "field": "age", "message": "must be at least 18" }
    ]
  }
}

// 404 Not Found
{
  "error": {
    "code": "not_found",
    "message": "User with ID 999 does not exist"
  }
}

// 429 Too Many Requests
{
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Retry after 30 seconds.",
    "retry_after": 30
  }
}

Which HTTP Status Codes Matter Most?

CodeMeaningWhen to Use
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST that creates a resource
204No ContentSuccessful DELETE with no response body
400Bad RequestMalformed JSON, missing required params
401UnauthorizedMissing or invalid authentication
403ForbiddenAuthenticated but lacks permission
404Not FoundResource does not exist
422Unprocessable EntityValid JSON but failed business validation
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnhandled server exception

How Should You Handle Authentication?

Use Bearer tokens (JWT or opaque) for stateless auth and API keys for server-to-server communication. Always transmit credentials over HTTPS. Include rate limiting and token expiration.

Authentication patterns
# Bearer token (JWT)
GET /v1/users/me
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...

# API key (server-to-server)
GET /v1/data/export
X-API-Key: sk_live_abc123def456

# OAuth 2.0 token exchange
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=AUTH_CODE&redirect_uri=...

What Is HATEOAS and Should You Use It?

HATEOAS (Hypermedia As The Engine Of Application State) embeds navigational links in responses so clients can discover related actions without hardcoding URLs. It is part of the REST maturity model (Level 3) but adds complexity — adopt it when your API has complex workflows or many interlinked resources.

HATEOAS response example
// GET /v1/orders/42
{
  "id": 42,
  "status": "shipped",
  "total": 99.99,
  "_links": {
    "self":     { "href": "/v1/orders/42" },
    "cancel":   { "href": "/v1/orders/42/cancel", "method": "POST" },
    "customer": { "href": "/v1/users/7" },
    "items":    { "href": "/v1/orders/42/items" },
    "invoice":  { "href": "/v1/orders/42/invoice", "type": "application/pdf" }
  }
}

Key Takeaways

  • • Use plural nouns for URIs and let HTTP methods express the action
  • • Version your API from day one — URI path versioning is the most common approach
  • • Prefer cursor-based pagination for large, frequently-changing datasets
  • • Return consistent error envelopes with machine-readable codes and field-level details
  • • Use appropriate HTTP status codes — never return 200 for errors
  • • Authenticate with Bearer tokens or API keys over HTTPS
  • • Consider HATEOAS for APIs with complex resource relationships

Frequently Asked Questions

How should I name REST API endpoints?

Use nouns (not verbs), plural form, kebab-case: /api/v1/blog-posts. Use HTTP methods for actions: GET (read), POST (create), PUT/PATCH (update), DELETE (remove). Nest related resources: /users/123/orders.

How should I version my API?

URL path versioning (/api/v1/) is the most common and explicit approach. Header versioning (Accept: application/vnd.api+json;version=1) is cleaner but harder to test. Pick one and be consistent.

How should I handle API errors?

Return appropriate HTTP status codes (400 for bad input, 404 for not found, 500 for server errors). Include a JSON body with error code, message, and details. Be consistent across all endpoints.

Was this helpful?

Stay up to date

Get notified about new guides, tools, and cheatsheets.