DEV Community

Cover image for A Practical Guide to Designing RESTful APIs
Fu'ad Husnan
Fu'ad Husnan

Posted on

A Practical Guide to Designing RESTful APIs

Designing RESTful APIs is one of those skills that separates developers who build systems that last from those who end up rewriting everything six months later. A well-designed RESTful API is predictable, consistent, and easy for other developers to consume without reading a wall of documentation. Whether you are building a public-facing API or an internal service, getting the fundamentals right from the start saves enormous headaches down the line.

This guide walks through the practical decisions you will face — from structuring your endpoints to handling errors gracefully — with real code examples to make things concrete.


Start With Resources, Not Actions

The single most important mental shift when designing a RESTful API is thinking in terms of resources rather than actions. Many developers who come from an RPC or SOAP background tend to design URLs that read like function calls. That is the wrong instinct here.

A resource is a noun — a thing your API exposes. An endpoint should represent that thing, and HTTP verbs (GET, POST, PUT, PATCH, DELETE) should describe what you are doing to it. This distinction keeps your API surface predictable for anyone who consumes it.

Consider a user management system. The resource-oriented approach looks like this:

GET    /users          → list all users
POST   /users          → create a new user
GET    /users/{id}     → get a specific user
PUT    /users/{id}     → replace a user entirely
PATCH  /users/{id}     → partially update a user
DELETE /users/{id}     → delete a user
Enter fullscreen mode Exit fullscreen mode

Compare that to the action-based antipattern: /getUser, /createUser, /deleteUser. These work, but they fight against the grain of HTTP and make the API harder to reason about at scale.

Handling Nested Resources

When one resource belongs to another, nesting reflects that relationship in the URL. An order that belongs to a user might live at /users/{userId}/orders. Keep nesting shallow — no more than two levels deep is a good rule of thumb. Deeply nested URLs become unwieldy and usually signal that you should reconsider your resource model.


Use HTTP Status Codes Correctly

One of the most common mistakes in API design is returning 200 OK for everything and burying the actual outcome in the response body. HTTP status codes exist precisely to communicate the result of a request at the protocol level, and clients — both human developers and automated systems — rely on them heavily.

The codes you will use most often fall into a few categories. 2xx codes mean success: 200 for a general success, 201 when a resource was created, 204 when a request succeeded but there is nothing to return (common for DELETE). 4xx codes indicate the client did something wrong: 400 for a malformed request, 401 when authentication is missing, 403 when the user is authenticated but lacks permission, 404 when the resource does not exist, 422 when the input is syntactically valid but semantically wrong. 5xx codes are for server-side failures.

Here is an example in Python using Flask that demonstrates this properly:

from flask import Flask, jsonify, request

app = Flask(__name__)

users = {1: {"name": "Alice", "email": "alice@example.com"}}

@app.route("/users/<int:user_id>", methods=["GET"])
def get_user(user_id):
    user = users.get(user_id)
    if not user:
        return jsonify({"error": "User not found"}), 404
    return jsonify(user), 200

@app.route("/users", methods=["POST"])
def create_user():
    data = request.get_json()
    if not data or "name" not in data or "email" not in data:
        return jsonify({"error": "Name and email are required"}), 400
    new_id = max(users.keys()) + 1
    users[new_id] = data
    return jsonify({"id": new_id, **data}), 201
Enter fullscreen mode Exit fullscreen mode

Notice that each route returns the appropriate status code alongside the response body. This makes it trivial for clients to handle responses programmatically without parsing the body just to find out if something went wrong.


Design Consistent and Meaningful Error Responses

Returning the right status code is half the battle. The other half is making sure your error responses are structured and informative. A bare 404 with no body leaves the client guessing. A well-structured error response tells them exactly what went wrong and — where possible — how to fix it.

A solid error response format includes a machine-readable error code, a human-readable message, and optionally a field reference if the error relates to specific input. Here is a pattern worth adopting:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "The request body is missing required fields.",
    "details": [
      { "field": "email", "issue": "This field is required." }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Consistency matters more than perfection here. Pick a format and use it across every endpoint. Nothing is more frustrating for an API consumer than error shapes that differ from one route to the next. Document your error format early and treat it as a contract.


Version Your API from Day One

APIs evolve. Features get added, requirements change, and sometimes you realize that a decision you made early was wrong. Versioning your API gives you the freedom to make breaking changes without pulling the rug out from under existing clients.

The most common approach is to include the version in the URL path: /v1/users, /v2/users. It is explicit, easy to route, and easy to document. Some teams prefer header-based versioning using a custom Accept header, but URL versioning wins on simplicity and visibility — developers can see the version at a glance.

GET /v1/users/42
GET /v2/users/42
Enter fullscreen mode Exit fullscreen mode

Start with v1 even if you think you will never change anything. You will change something. Having the version baked in from the beginning costs almost nothing and gives you enormous flexibility later.

Deprecation Strategy

When you introduce a new version, do not immediately kill the old one. Give consumers a deprecation window — communicate it clearly via documentation, response headers, and direct outreach if you have registered API users. A Deprecation response header or a Sunset header can signal to clients programmatically that they should migrate.


Handle Pagination, Filtering, and Sorting

Any endpoint that returns a collection needs to be paginated. Returning unbounded lists is a performance trap that will hurt both your server and your clients. There are two common pagination styles: offset-based and cursor-based.

Offset-based pagination uses page and limit query parameters and is straightforward to implement and understand. Cursor-based pagination uses an opaque cursor token pointing to a position in the dataset, which handles large datasets more efficiently and avoids the "page drift" problem where items shift between pages as new records are added.

A simple offset-based example:

GET /users?page=2&limit=25
Enter fullscreen mode Exit fullscreen mode

The response should include metadata so clients know where they are:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "limit": 25,
    "total": 134,
    "total_pages": 6
  }
}
Enter fullscreen mode Exit fullscreen mode

Filtering and sorting follow naturally from the same query string approach. Keep the parameter names intuitive: ?status=active, ?sort=created_at&order=desc. Do not over-engineer this early — support the filters your consumers actually need, and add more as requirements become clear.


Secure Your API Thoughtfully

Security is not an afterthought you bolt on after the API is designed — it is a design concern from the very first endpoint. Authentication and authorization need to be part of your mental model from day one.

JWT (JSON Web Tokens) has become the de facto standard for stateless authentication in REST APIs. The client authenticates once, receives a signed token, and includes it in subsequent requests via the Authorization header. Here is the basic pattern:

Authorization: Bearer <your_jwt_token_here>
Enter fullscreen mode Exit fullscreen mode

On the server side, you validate the token's signature and extract the claims — user ID, roles, scopes — before processing the request. Never trust user-supplied IDs without verifying that the authenticated user actually has access to the requested resource. This is the difference between authentication (who are you?) and authorization (what are you allowed to do?).

Rate limiting is equally important. Without it, a poorly written client or a malicious actor can bring your API to its knees. Return 429 Too Many Requests when limits are hit, and include headers like Retry-After so clients know when to back off.


Document as You Build

The best API documentation is not written after the API is finished — it is written alongside it. Tools like OpenAPI (formerly Swagger) let you define your API contract in a structured format that can generate interactive documentation, client SDKs, and even mock servers automatically.

Here is a minimal OpenAPI snippet for a user endpoint:

paths:
  /users/{id}:
    get:
      Summary: Get a user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: User found
        "404":
          description: User not found
Enter fullscreen mode Exit fullscreen mode

Keeping your OpenAPI spec in sync with your actual implementation becomes much easier when you treat the spec as the source of truth and generate server stubs or validation middleware from it, rather than writing the spec after the fact.


Conclusion

Designing RESTful APIs well is a discipline that pays dividends for every developer who touches your system. The principles covered here — resource-oriented URLs, correct HTTP semantics, consistent error responses, versioning, pagination, security, and documentation — are not arbitrary rules. They exist because they solve real problems that every API eventually runs into.

Start with these foundations, resist the temptation to over-engineer early, and iterate based on what your consumers actually need. If you are building an API today, audit your existing endpoints against these principles and pick one area to improve. Small, deliberate improvements compound quickly, and a well-designed API is one of the most valuable things you can deliver as a developer.

Top comments (0)