REST API Design Basic Patterns: A Practical Beginner's Guide



REST API Design Basic Patterns: A Practical Beginner's Guide

Quick Summary

REST APIs work best when they are designed around resources, not actions, and when they follow consistent patterns for URLs, HTTP methods, and responses. In this post, we walk through the core design patterns that make REST APIs predictable and easy to use.

You will learn how to design clean resource URLs, map operations to HTTP methods, choose appropriate status codes, and structure JSON responses. We also cover common patterns for pagination, filtering, sorting, and versioning so that your API scales with real-world use.

The examples use simple, realistic endpoints (such as /v1/users and /v1/posts/{postId}/comments) to show the patterns in action. Even if you are new to REST, you can follow step by step and apply the patterns to your own API.

Table of Contents


Key takeaways

  • Design URLs around nouns (resources), not verbs. Use clear, plural resource names like /v1/users and avoid RPC-style paths such as /doCreateUser.
  • Use HTTP methods consistently: GET for read, POST for create or non-idempotent actions, PUT/PATCH for updates, and DELETE for deletions. This makes the API self-explanatory.
  • Return meaningful status codes and error bodies. Combine 2xx, 4xx, and 5xx codes with a predictable JSON error structure so clients can handle failures reliably.
  • Adopt standard patterns for pagination, filtering, and sorting using query parameters like ?page=1&size=20 or ?limit=20&offset=40, and include metadata in the response.
  • Plan for versioning early, usually with a version in the URL path (e.g., /v1, /v2), and keep old versions stable until you officially deprecate them.
  • Document your conventions and stick to them. A simple, consistent rule set is more valuable than a complex "perfect" design that no one can remember.

In-depth explanation of REST API design patterns

1. What makes an API "RESTful"?

"REST" stands for Representational State Transfer. In practice, a RESTful API exposes resources (such as users, orders, or posts) through standard HTTP methods and representations like JSON.

Typical REST characteristics include:

  • Resource-based: The central concept is the resource (e.g., /users, /orders/123), not actions.
  • Statelessness: Each request contains all information needed to process it. The server does not store client session state between requests.
  • Uniform interface: The same methods (GET, POST, PUT, PATCH, DELETE) are used consistently across resources.
  • Representation-oriented: Clients interact with representations (JSON, XML, etc.) rather than internal database structures.

In day-to-day development, you do not need to follow every academic detail of REST. What matters most are consistent patterns that make the API easy to guess and hard to misuse.

2. Resource-oriented URL design patterns

Good REST URLs are simple, descriptive, and stable. Common patterns include:

  • Use nouns, not verbs
    Good: /v1/users, /v1/orders/123, /v1/posts/42/comments
    Avoid: /v1/createUser, /v1/getAllUsers, /v1/deleteOrderById
  • Use plural resource names
    Plural forms like /users, /orders signal that the endpoint represents a collection. A single item is represented by appending its identifier: /users/{userId}.
  • Use path hierarchy for natural relationships
    For example, comments belong to a post:
    • List comments for a post: /v1/posts/{postId}/comments
    • Single comment: /v1/posts/{postId}/comments/{commentId}
  • Keep URLs stable and semantic
    Avoid including implementation details such as /mysql or /internal in the public URL. URLs should still make sense years later even if the backend changes.

When you need to express an action that does not fit the standard CRUD pattern, consider using a sub-resource or action-like noun. For example, instead of /v1/orders/123/cancelOrder, prefer /v1/orders/123/cancellation with POST.

3. HTTP methods and CRUD mapping patterns

One of the most important REST patterns is a predictable mapping between HTTP methods and operations:

  • GET: Read resource(s). Must not change server state (safe) and should be idempotent.
    • GET /v1/users – list users
    • GET /v1/users/{userId} – get details of a specific user
  • POST: Create a new resource or trigger a non-idempotent action.
    • POST /v1/users – create a new user
    • POST /v1/orders/{orderId}/cancellation – request order cancellation
  • PUT: Replace an existing resource with the given representation. Typically idempotent.
    • PUT /v1/users/{userId} – fully update user information
  • PATCH: Partially update an existing resource (e.g., one or two fields).
    • PATCH /v1/users/{userId} – update only some fields, such as email
  • DELETE: Remove an existing resource.
    • DELETE /v1/users/{userId} – delete a user

The key design rule is: for a given resource, choose a mapping and reuse it everywhere. If POST /v1/users creates a user, then POST /v1/orders should create an order as well, not perform an unrelated action.

4. Response structure and status code patterns

Status codes and response bodies form another important design pattern. A simple, consistent mapping could be:

  • 2xx for success
    • 200 OK – successful read or update
    • 201 Created – resource created, often with a Location header
    • 204 No Content – successful operation with no response body (commonly used for DELETE)
  • 4xx for client errors
    • 400 Bad Request – malformed input
    • 401 Unauthorized – missing or invalid authentication
    • 403 Forbidden – authenticated but not allowed
    • 404 Not Found – resource does not exist
    • 409 Conflict – state conflict (e.g., duplicate email)
    • 422 Unprocessable Entity – validation errors on well-formed data
  • 5xx for server errors
    • 500 Internal Server Error – unexpected server-side failure
    • 503 Service Unavailable – temporary overload or maintenance

For error responses, define a standard JSON structure such as:

{
  "error": "validation_failed",
  "message": "Email is invalid.",
  "details": {
    "email": ["Email format is not correct."]
  },
  "requestId": "c0a8012f-1234-4bcd-9def-000000000001"
}

Using a consistent error format makes it easy for frontend or client developers to parse and display issues, log them, and handle different error types in a unified way.

5. Pagination, filtering, and sorting patterns

Real-world APIs often return many items. Common patterns for handling large collections include:

  • Offset-based pagination with limit and offset:
    • GET /v1/users?limit=20&offset=40
    • Simple and widely understood, but not ideal for very large datasets.
  • Page-based pagination with page and size:
    • GET /v1/users?page=3&size=20
    • Natural for UI pages, easier to show "Page 3 of 10".
  • Cursor-based pagination for high-scale APIs:
    • GET /v1/users?cursor=eyJpZCI6NTB9&limit=20
    • More complex but stable under concurrent inserts and deletes.

For filtering and sorting, a simple pattern is to use query parameters:

  • Filtering: /v1/users?role=admin&status=active
  • Sorting: /v1/users?sort=createdAt,desc

Whichever pattern you adopt, include metadata in the response (for example, total, page, size, hasNext) so clients do not have to guess how many items remain.

6. Versioning patterns

APIs evolve over time. A basic pattern is to include the version in the URL path:

  • /v1/users, /v1/orders
  • Later: /v2/users with a different schema or behavior

URL-based versioning is easy to understand, easy to route, and works well with documentation. Alternative patterns include:

  • Header-based versioning (for example, Accept: application/vnd.example.v1+json)
  • Query parameter based versioning (for example, ?version=1)

For most teams, path-based versioning is the simplest way to start. Whatever you choose, define clear rules for how long you will support old versions and how you will communicate deprecations to API consumers.

7. Common mistakes to avoid

When designing REST APIs, beginners often fall into a few common traps:

  • Mixing different styles: Combining RPC-style endpoints (/doSomething) with resource-oriented ones makes the API confusing.
  • Overloading POST for everything: Using POST for reads, updates, and deletes ignores HTTP semantics and complicates caching and tooling.
  • Inconsistent naming: Using /user in one place and /users in another, or changing field names without strong reason.
  • Insufficient error messages: Returning 500 for all problems, or no explanation in the body, makes debugging difficult.
  • Not thinking about pagination and limits: Returning thousands of records in one response can cause performance issues and timeouts.

Being aware of these pitfalls early will help you design APIs that scale smoothly and remain friendly for clients.


Code examples

The following examples show how the patterns described above look in real requests and server-side code.

# List users with pagination and filtering
curl -X GET "https://api.example.com/v1/users?role=admin&page=1&size=20" \
  -H "Accept: application/json"

# Create a new user
curl -X POST "https://api.example.com/v1/users" \
  -H "Content-Type: application/json" \
  -d '{
        "email": "jane.doe@example.com",
        "name": "Jane Doe",
        "role": "admin"
      }'

The first request uses GET with query parameters for pagination and filtering. The second uses POST to create a new user with a JSON body. Both follow a clear pattern: resource-based URLs, standard HTTP methods, and JSON for input and output.

// Example using Node.js and Express

const express = require("express");
const app = express();

app.use(express.json());

// GET /v1/users
app.get("/v1/users", (req, res) => {
  // In a real app, query the database with filters and pagination.
  const users = [{ id: 1, name: "Jane Doe", email: "jane@example.com" }];
  res.status(200).json({
    data: users,
    page: 1,
    size: users.length,
    total: users.length
  });
});

// POST /v1/users
app.post("/v1/users", (req, res) => {
  const { email, name } = req.body;
  if (!email || !name) {
    return res.status(422).json({
      error: "validation_failed",
      message: "Name and email are required."
    });
  }

  const createdUser = { id: 2, email, name };
  res
    .status(201)
    .location(`/v1/users/${createdUser.id}`)
    .json(createdUser);
});

// GET /v1/users/:id
app.get("/v1/users/:id", (req, res) => {
  const { id } = req.params;
  // Lookup user by id
  if (id !== "2") {
    return res.status(404).json({
      error: "not_found",
      message: "User not found."
    });
  }
  res.status(200).json({ id: 2, email: "jane@example.com", name: "Jane Doe" });
});

app.listen(3000, () => {
  console.log("REST API listening on port 3000");
});

This Express example shows how to implement typical REST patterns: defining collection and item routes, returning structured JSON responses, setting appropriate status codes, and using the Location header after creating a resource.


Step-by-step design checklist

  1. Define your domain and core resources
    Start by listing the main entities in your system, such as users, products, orders, or posts. Decide which of them will be exposed as first-class API resources and which ones will be internal only.
  2. List operations for each resource
    For every resource, write down what clients need to do: list, read, create, update, delete, and any special actions. Map each operation to an HTTP method following the patterns from the previous sections.
  3. Design clean, consistent URLs
    For each operation, design a URL using plural nouns and simple hierarchies. Ensure that similar resources follow the same naming rules and path structure across the whole API.
  4. Define request and response schemas
    For each endpoint, specify the JSON structure: required and optional fields, types, and constraints. Align field names with your domain language and keep them consistent between create, read, and update endpoints.
  5. Choose pagination, filtering, and sorting patterns
    Decide whether you will use page/size, limit/offset, or cursor-based pagination. Standardize query parameter names (for example, page, size, sort) and apply them consistently.
  6. Standardize error handling
    Agree on a small set of status codes and a shared error response format. Document examples for validation errors, not-found errors, and server errors so that every endpoint behaves in the same way.
  7. Plan versioning strategy
    Decide how you will represent versions (path, header, or another method) and how you will migrate clients when breaking changes are needed. Start with /v1 and keep compatibility for as long as necessary.
  8. Document and review the API
    Use an API description format such as OpenAPI (Swagger) to document endpoints, parameters, and responses. Review the design with both backend and frontend engineers to validate that the patterns are easy to understand.

Pattern comparison table

The table below summarizes common REST API design areas and typical pattern choices for each.

Area Pattern Typical choice Pros Cons / Considerations
Resource URLs Noun-based, plural resources /v1/users, /v1/orders/123 Easy to guess, aligns with REST principles, tool-friendly Requires some design effort to model resources cleanly
HTTP methods CRUD mapped to GET/POST/PUT/PATCH/DELETE GET for read, POST for create, etc. Predictable for clients, works well with caching and proxies Special actions may need additional patterns (sub-resources)
Status codes Small, consistent subset 200, 201, 204, 400, 401, 403, 404, 409, 422, 500 Simplifies error handling and documentation Requires discipline to avoid inconsistent codes across teams
Pagination Page/size or limit/offset query params ?page=1&size=20 or ?limit=20&offset=40 Simple to implement and easy for frontends to use Offset-based may not scale well for very large datasets
Versioning Version in URL path /v1, /v2 Clear, easy to route and document Requires migration plan when deprecating older versions

Further topics to consider

  • Authentication and authorization – Decide early on whether you will use OAuth 2.0, JWTs, API keys, or another mechanism, and how permissions are enforced across resources.
  • Observability and logging – Design request IDs, logging formats, and metrics from the beginning so you can debug issues in production and understand API usage patterns.
  • Rate limiting and quotas – To protect your service from abuse and accidental overload, define fair usage limits per client and clear error responses when limits are exceeded.
  • Consistent documentation and SDKs – Well-designed APIs become much easier to adopt when you provide interactive documentation and client libraries that follow the same patterns.


Reactions

댓글 쓰기

0 댓글