Best practices to design REST APIs the right way

As a web developer, I’ve spent countless hours both building and consuming REST APIs. Like everything in our field, they’re not always designed and structured properly (and I’ll admit, I haven’t always gotten them right either). A well-crafted, maintainable project depends on getting these details right—mastering these concepts is what separates a solid project from a poorly executed one.

I often say that we developers are craftsmen of the intangible, and the quality of our work shows in how skillfully we shape our applications.

This article isn’t the final word on the subject—it’s a starting point, a waystation, a collection of best practices and guidelines you should know to build quality APIs.

Why Do We Write APIs?

The answer is straightforward: someone (or something) needs to interact with our system. Our job is to build an access road into our system. Would you enjoy driving on a pothole-riddled road that constantly changes routes? I certainly wouldn’t. Writing good REST APIs means building a smooth, reliable highway.

The best API is one that developers can use without reading the documentation

Think Before You Code

Developing without thinking is the surest path to terrible code.

Before writing a single line, you need to step back and think about the context of your endpoints: how to organize them, and crucially, how they’ll evolve. Skip this step, and you’ll end up building that pothole-riddled road.

API analysis typically follows two approaches:

  • Bottom-up: Start with existing entities and build endpoints from them. Relationships are already known—this approach works when you have an existing product and want to create generic REST APIs for clients or various integrations.
  • Top-Down: Start with functional analysis (e.g., feature mockups or UI designs) then build the APIs. These are purpose-built for a known solution. This approach branches into patterns like BFF (Backend-for-Frontend, where APIs are tailored for specific UIs) or contract-first design (where API contracts are created before implementation).

Analysis isn’t wasted time—it’s an investment in your product’s future. I’ve seen too many developers jump straight into coding without thinking it through. I can assure you, the results were disastrous.

Design Principles

Resources

When designing APIs, think in terms of resources, not actions. Actions are identified by verbs (we’ll get to those), while resources are the modules or components of your application.

If we were building integration APIs for R2-D2, the messages, resources, lightsaber launcher, and radar would all be resources in its internal operating system.

In a typical web application, resources might be users, employment contracts, orders, vehicles, etc.

Actions(verbs)

Verbs define an endpoint’s purpose, and every endpoint must have one. The verbs we use are:

  • GET: Retrieve a resource (e.g., return list of messages)
  • POST: Create a new resource or trigger an action (e.g., launch lightsaber)
  • PUT: Replace/fully update an existing resource (e.g., update multiple message properties)
  • PATCH: Partially update an existing resource (e.g., update only message recipient)
  • DELETE: Remove a resource (e.g., delete specific message)

Some API examples:

[GET] /users/123/details[GET] /users/123
[POST] /users/123/delete[DELETE] /users/123
[POST] /users/123/update-email[PATCH] /users/123

Endpoint formatting

To keep endpoints clean, organized, and standard, follow these rules:

Resource names should always be plural

❌​ /user

/users

Case-insensitive design: always use lowercase

❌​ /Users

/users

    Access specific resources by placing the ID immediately after the resource

    /users/123/contracts

    /users/123/contracts/58

    Compound words: separate with hyphens (not underscores or camelCase)

    ❌​ /userContracts

    ❌​ /user_Contracts

    /users-contracts

    Follow dependency hierarchy from parent to child entities

      Consider these entities: Users, contracts, and user contracts.

        When designing APIs, we’d have:

        /api/users References users
        /api/contracts References generic employment contracts users can sign
        /api/users/{id}/contracts References contracts signed by a specific user
        /api/users/{id}/contracts/{id}/documentsDocuments for a specific user’s contract

        Filtering, Sorting, and Pagination

        This deserves its own section.

        Imagine a search endpoint with 8 filter parameters. Following what we’ve discussed, you might think:

        [GET] /users/{filter1}/{filter2}/{filter3}/{filter4}/{filter5}/{filter6}/{filter7}/{filter8}

        What if you only need one filter? Or two? Clearly, this isn’t the right approach. Search parameters should use query strings because they’re optional (only include what’s needed) and don’t change the endpoint structure.

        From an evolutionary perspective, this lets us maintain the same endpoint (/users) while adding new filters as the software grows:

        [GET] /users?filterName=Giorgio&filterIsActive=true

        Now let’s talk pagination. Never build a search endpoint without pagination for two reasons:

        1. Software evolves—today your list has 10 items, tomorrow 10,000
        2. Without pagination, you force both frontend and backend to handle massive datasets, causing performance issues

        Page number and page size become query string parameters:

        [GET] /users?filterName=Giorgio&filterIsActive=true&pageNumber=2&pageSize=30

        Adopting this strategy across all APIs provides a standard way to handle collection endpoints. It’s not just about clean, organized code—you’re giving users a consistent way to interact with these endpoints.

        I’ve worked with systems where different APIs handled collections differently—some used POST methods, others had filters in query strings, some had none at all. This creates confusion.

        One final piece: the response object. For paginated results, APIs should always respond with a standard object:

        Response
        {
          "items": [],
          "totalItems": 0,
          "page": 0,
          "pageSize": 0,
          "totalPages": 0
        }

        With this object, a frontend has everything needed to manage pagination components, determine if it’s on the first or last page, and know the total element count.

        Remember: we’re building a way for someone to access our system. If we don’t use a “standard” approach, we’re just building chaos—and the world has enough of that already!

        Versioning

        If your product’s APIs never need updates, business probably isn’t going well! 😊

        Seriously though, systems evolve, and so must our APIs. We need to indicate which version is being used.

        There are two main approaches:

        • Include version in the URL
        • Include version as a header key

        I’ve used both, but URL versioning is more common and my preference—it’s clearer and more readable:

        [GET] /v1/users

        [GET] /v1.2/users

        [GET] /v2/users

        Version numbers also indicate breaking changes in parameters or returned objects. If you have a v1 API and introduce logic changes in data returns, create v1.1. But if changes modify the returned object (removed fields or new structure), you need v2.

        Maintaining multiple API versions means managing backend compatibility. How many versions to maintain depends on your environment and context. Generally, we deprecate old API versions (with plenty of advance notice to clients!) to keep the backend clean.

        Some contexts don’t allow removing old APIs—like vending machines or legacy hardware without OTA updates. API evolution must be planned upfront, and you can manage obsolescence by making fields non-mandatory in new versions. Each context needs analysis—there’s no universal answer. This brings us back to the importance of the analysis phase!

        Http Status Codes

        Someone invented the web before us, including HTTP status codes for us future developers to use. Now, who here regularly (and correctly) uses these codes? 🙋

        Here’s what the codes mean by their first digit:

        1xxRequest received, process continues. Rarely used in APIs
        2xxRequest received, understood, and successfully accepted. This is what we want!
        3xxClient must take additional action to complete the request
        4xxRequest contains bad syntax or cannot be fulfilled. Error caused by the API caller
        5xxServer failed to fulfill an apparently valid request. Backend error

        The most commonly used codes:

        CodeWhatWhen
        200OKRequest succeeded. Standard for GET. Can be used for PUT/PATCH if response body contains updated resource
        201CreatedRequest succeeded and new resource created. Response to POST that generated new resource. Must include Location header with new resource URL
        204No Contenterver processed request successfully but returns no content. Ideal for successful DELETE. Also useful for PUT/PATCH when returning updated resource isn’t necessary
        CodeWhatWhen
        400Bad Request        Server can’t process request due to client error. E.g., malformed JSON, validation failures, missing required parameters
        401 Unauthorized        Someone’s calling our APIs without authentication token (e.g., JWT), or it’s expired/invalid
        403Forbidden        Client is authenticated but lacks permissions for requested action
        404Not Found        The classic. If calling /users/123 but user 123 doesn’t exist, return 404
        409Conflict        Attempting to create something that already exists (e.g., user with same username)
        CodeWhatWhen
        500Internal Server Error        Generic unexpected server error occurred. Check the logs
        503Service Unavailable        Server temporarily unable to handle request. Time to call ops

        Security

        Throughout my career, I’ve written many APIs (and will write many more, I hope!). The only ones without authentication were exposed within a corporate network or well-defined perimeter. Leaving unsecured APIs exposed to the internet is like leaving your window permanently open—it might seem “convenient” (never have to close it!), but eventually someone will break in.

        This topic could fill an entire book, but at a high level, we can identify five main elements:

        AUTHeNthication

        When setting up authenticated APIs, we need an authentication provider (AuthN)—a tool that validates API keys or credentials. On successful validation (HTTP 200!), we receive an authentication token (typically JWT) or API key to include in the “Authorization” header of our requests.

        AUTHoriZation

        Authorization (AuthZ) follows authentication (AuthN) and answers “What can you do?” It determines user roles and permissions within a system.

        If a user deletion API requires “admin” permission and I lack it, I’ll receive a 403 error.

        Authorization strategies fall into three main categories:

        • RBAC (Role-Based-Access-List) – Most common; capabilities determined by user roles
        • ABAC (Attribute-Based-Access-List) – More flexible but complex than RBAC, especially in enterprise applications where user-resource permission management can become complex
        • ACL (Access-Control-List) – Each resource has dedicated permissions (e.g., Google Docs with per-document permission management)

        Complex systems often use a mix of these approaches.

        Rate Limiting

        When designing APIs, consider that someone might “fall in love” with your endpoint and call it repeatedly. This causes application slowdowns or triggers cloud auto-scaling mechanisms (with associated costs).

        To prevent this, implement request limits (per minute or second, depending on endpoint function) with automatic IP suspension mechanisms.

        Input validation

        This seems obvious, but input validation should happen first to avoid unnecessary processing that would fail halfway through (or worse, create anomalous situations).

        Imagine an endpoint updating user emails: without upfront email validation, we might update the data only to receive an error from the mail service due to an invalid address.

        CORS

        .NET developers who migrated from .NET Framework to .NET Core definitely encountered this.

        CORS (Cross-Origin Resource Sharing) is a strategy defining who can call our APIs by specifying which domain(s) we accept when someone calls our REST APIs.

        APIs Are Stateless

        My first steps in web development were with ASP.NET applications (someday someone will ask “what was ASP.NET?”), where I managed session information (stored by good old IIS). After login, I could save session information tied to users and use it wherever needed.

        Then came the cloud.

        And with it, the need for scalability.

        This solidified the “stateless” concept as the baseline for web applications.

        REST APIs maintain no user state in memory. Users are identified through JWT tokens (or other strategies), and capabilities are determined accordingly.

        Behind the Scenes

        Idempotency

        The first time I heard this word, I wondered what arcane meaning it held. Today, it’s a fundamental concept when creating endpoints (or what I expect when calling APIs).

        An endpoint is idempotent if the result doesn’t change with multiple calls.

        Creating idempotent APIs is crucial in some contexts—failing to do so triggers multiple processes that could lead to data loss or unexpected behaviors.

        Imagine developing an online payment endpoint used by an Angular application. A customer tries to pay €100, but their connection falters.

        They press pay again—nothing happens.

        Frustration

        Click again—still nothing.

        Anger

        They compulsively mash the “Pay” button.

        The connection recovers and all 24 clicks succeed. What happens without idempotent endpoints? Instead of one payment, we process 24, creating customer distress and damaging company reputation.

        Caching

        One great advantage of REST APIs is using caching mechanisms to avoid reprocessing recently handled requests. Not all data changes frequently—some have low update rates (like the number of satellites orbiting Earth or countries in the world).

        In enterprise contexts, caching is managed by API gateways; in others, we implement it ourselves. All major frameworks offer cache management options.

        Benefits of implementing cache:

        • Faster API responses (avoiding calculation or data reading)
        • Avoiding reprocessing requests we’ve already answered
        • In cloud environments, not processing unnecessary requests means savings (thousands of requests = guaranteed savings!)

        Monitoring and Observability

        In 2025, it’s unthinkable to create new enterprise APIs without monitoring or log tracing. Applications grow over time, endpoints multiply, and we scramble to fix issues because we don’t know which endpoints are most called, which are ignored, and which error most frequently (true story).

        Many tools exist, both free and paid, for monitoring APIs. Which to use depends on your business context.

        I’ve used both Prometheus+Grafana and ELK—both excellent open-source systems.

        Documentation

        This is the last point in this long article but also one of the most important. Documentation is fundamental both for us (how often do we develop something undocumented and forget what it does after a month?) and for clients (if an endpoint isn’t documented, how can they understand it?).

        I’ll cover this in a dedicated article, but I worked with a company that had a product (30 years of development) completely undocumented. The result? The same features were developed repeatedly because nobody remembered they’d already been implemented.

        Conclusions

        Today, between AI and various resources, writing good APIs is a choice. Not doing so condemns a service from the start, accumulating unnecessary technical debt. APIs are our calling card—corporate or personal—through which we enable connections.

        I hope this article was interesting—maybe it refreshed knowledge tucked away in memory, maybe it taught you something new.

        If you found it useful, send it to your struggling colleague—it might help them grow, and who knows, maybe you’ll get less frustrated seeing certain endpoints!

        Until next time!

        Share this article
        Shareable URL
        Prev Post

        EF Core 10: .leftJoin() and .rightJoin() in LINQ

        Next Post

        Architecture and use case: how to building a dynamic UI entities system for enterprise applications

        Read next

        FluentValidation

        Throughout my career as a developer, I’ve built, maintained, and extended hundreds of .NET applications.…
        fluent validation article header

        Hybrid Cache released!

        Hey devs! Great news – .NET 9.0.3 just dropped a few days ago, and with it comes the long-awaited official…
        Hybrid cache released header image