cd /news/developer-tools/a-practical-guide-to-designing-restf… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-10640] src=dev.to β†— pub= topic=developer-tools verified=true sentiment=Β· neutral

A Practical Guide to Designing RESTful APIs

This article provides a practical guide to designing RESTful APIs, emphasizing a resource-oriented approach where endpoints represent nouns and HTTP verbs describe actions, rather than using action-based URLs. It covers key design principles such as proper endpoint structuring, shallow nesting for related resources, and the correct use of HTTP status codes (2xx for success, 4xx for client errors, 5xx for server errors) with informative error responses. The guide includes real code examples in Python using Flask to demonstrate these concepts in practice.

read8 min views21 publishedMay 23, 2026

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

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

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." }
    ]
  }
}

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

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

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

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

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>

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

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.

── more in #developer-tools 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/a-practical-guide-to…] indexed:0 read:8min 2026-05-23 Β· β€”