Need the #1 custom application developer in Brisbane?Click here →

REST API Design Principles

10 min read

REST (Representational State Transfer) is an architectural style for APIs. It uses HTTP methods and URLs to define operations. A well-designed REST API is intuitive, consistent, and a pleasure to consume. A poorly designed one is confusing and error-prone.

REST's strength is its simplicity and standardization. Developers immediately understand how to interact with a REST API because they follow conventions. GraphQL offers more flexibility but less standardization. For most APIs, REST is the right choice.

Resources and URLs

REST APIs are organized around resources. A resource is a noun: users, orders, products, comments. The URL identifies the resource:

  • /users — collection of users
  • /users/123 — user with ID 123
  • /orders — collection of orders
  • /users/123/orders — orders belonging to user 123

URLs are nouns, not verbs. Bad: /getUser, /createOrder. Good: /users, /orders. The HTTP method specifies the action.

HTTP Methods

MethodMeaningIdempotentSafeExample
GETRetrieve a resourceYesYesGET /users/123
POSTCreate a new resourceNoNoPOST /users (with new user data)
PUTReplace entire resourceYesNoPUT /users/123 (with updated user data)
PATCHPartial update to resourceNoNoPATCH /users/123 (with fields to update)
DELETERemove a resourceYesNoDELETE /users/123

Idempotent: calling the method multiple times with the same request has the same effect as calling once. PUT is idempotent—calling it twice replaces the resource twice with the same value, same result. POST is not—calling it twice creates two resources.

Safe: the method doesn't modify server state. GET is safe (just retrieving). DELETE is not safe (it removes data).

Note
Use the right HTTP method. Don't use GET for creating data (it's not safe). Don't use POST for retrieving data (it's not safe). The methods have semantic meaning that both humans and tools rely on.

HTTP Status Codes

Status codes communicate the result of the request:

  • 2xx (Success): 200 OK (request succeeded), 201 Created (resource created), 204 No Content (request succeeded, no body)
  • 3xx (Redirect): 301 Moved Permanently, 304 Not Modified (cached version is still valid)
  • 4xx (Client error): 400 Bad Request (invalid request), 401 Unauthorized (authentication required), 403 Forbidden (authenticated but not allowed), 404 Not Found (resource doesn't exist)
  • 5xx (Server error): 500 Internal Server Error (something went wrong), 503 Service Unavailable (temporarily down)

Using the correct status code matters. A client library might retry on 5xx but not 4xx. Proper status codes enable proper error handling.

Request and Response Bodies

JSON is the standard for API bodies. Send and receive JSON. Be consistent about structure:

POST /users
{
  "email": "user@example.com",
  "name": "Alice"
}

Response (201 Created):
{
  "id": 123,
  "email": "user@example.com",
  "name": "Alice"
}

Envelope structure: should responses be wrapped in an envelope? { "data": [...] } or just the data? Either works, but be consistent.

Versioning

APIs change. New fields are added, endpoints are removed, behaviors change. How do you handle this without breaking existing clients?

Option 1: URL versioning. /api/v1/users, /api/v2/users. Clear, but requires maintaining multiple versions.

Option 2: Header versioning. Accept: application/vnd.api+json;version=1. Cleaner URLs, but less discoverable.

Option 3: No explicit versioning, but graceful evolution. New fields are additive (never break existing fields), optional parameters are accepted. Clients ignore fields they don't understand.

Plan for versioning from day one. Adding versioning retroactively is painful. Most APIs use URL versioning for simplicity.

Pagination

Never return unlimited records. Queries on large tables are slow. Send too much data and clients waste bandwidth. Paginate:

GET /users?page=1&limit=20

Response:
{
  "data": [...],
  "total": 1000,
  "page": 1,
  "limit": 20
}

Offset-based pagination (page/limit) is simple but inefficient for large offsets. Cursor-based pagination (using a cursor to mark the starting point) is more efficient for large datasets.

Error Responses

Errors need clear messaging. Bad: { "error": "Invalid input" }. Good:

{
  "code": "INVALID_EMAIL",
  "message": "Email must be a valid email address",
  "details": { "field": "email" }
}

Include: an error code (INVALID_EMAIL, not just "error"), a message explaining the issue, optional details about which field/resource had the problem.

Authentication

APIs (consumed by non-browser clients) typically use bearer tokens in the Authorization header:

Authorization: Bearer eyJhbGc...token

The token is usually a JWT (JSON Web Token) that includes claims about the user (id, permissions) that the server can verify without hitting the database.

For browser-based clients, use secure cookies. For APIs, use bearer tokens.

Following Conventions

REST conventions reduce cognitive load for developers using your API. They immediately understand how to interact with it. Deviating from conventions confuses users and wastes time. Unless you have a strong reason, follow REST conventions.

Good API design is about making the API intuitive and predictable. A developer should be able to guess how to use your API based on what they know about REST. If they can't, you've made the API too custom.

Tip
API design is part of product design. Your API is consumed by developers. Make their experience smooth. Good API design contributes to developer happiness and adoption.

Documentation

A brilliant API is useless without documentation. Document every endpoint: what it does, what parameters it accepts, what it returns, what errors might occur.

Tools like Swagger/OpenAPI generate interactive documentation from your API code. This is valuable for developers learning your API and for testing.