Backend from First Principles: Complete REST API Design


API design is a critical skill for any backend engineer, and it's a topic you'll spend a significant amount of time working on and thinking about. This post will focus primarily on REST API design, one of the most widely used API standards. While there are other technologies for building APIs like RPC and GraphQL, our focus here will be exclusively on REST.

Why REST API Design Matters: Addressing Common Confusions

Despite REST being a common standard with years of research and developer experience behind it, engineers, especially those new to backend development, often face confusion. Questions frequently arise such as:

  • Should URI path segments be plural or singular?

  • When updating a resource, should you use PATCH or PUT HTTP methods?

  • For non-CRUD (Create, Read, Update, Delete) operations, often called "action calls," which HTTP method is appropriate?

  • What HTTP status code should be used for different scenarios?

This confusion often stems from the fact that when these widespread HTTP API standards were developed, the state of the internet, web, clients, and servers was very different from today. Previously, there was a heavy reliance on Multi-Page Applications (MPAs). In contrast, modern web development heavily utilizes Single-Page Applications (SPAs), where the browser downloads all necessary JavaScript with the first API call and handles client-side routing.

The purpose of this guide is not to create new standards, but to extract clear rules and guidelines from existing REST standards. By adhering to these guidelines, we aim to establish a consistent styling pattern for designing APIs, payloads, and documentation. This consistency allows backend engineers to streamline development and focus on business logic, rather than constantly questioning API restfulness or adherence to the latest industry standards.

A Brief History of the Web and REST

To better understand REST API design, it's helpful to know its origins:

The Genesis of the World Wide Web

In 1990, Tim Berners-Lee initiated a project called the World Wide Web with the primary motivation of sharing knowledge globally. Within about a year, he invented several foundational concepts and technologies that we still use today (though in more advanced versions):

  • URI (Uniform Resource Identifier): A fundamental concept for identifying resources.

  • HTTP (Hypertext Transfer Protocol): The underlying protocol for client-server communication. (We've covered HTTP in a previous video).

  • HTML (Hypertext Markup Language): The markup language used to construct web pages, forming the skeleton of a page.

  • The first web server.

  • The first web browser.

  • The first "What You See Is What You Get" (WYSIWYG) HTML editor, built directly into the browser.

The Web's Scalability Crisis and Roy Fielding's Contributions

Soon, the World Wide Web faced a breakdown due to the exponential growth of its user base. The initial design and planning did not account for such a massive scale.

Around 1993, Roy Fielding, a co-founder of the Apache HTTP server project, became concerned about this web scalability problem. To address this, he proposed a set of architectural constraints to make the World Wide Web more scalable. These constraints later became the guiding principles of REST.

Roy Fielding's Architectural Constraints:

  1. Client-Server: This constraint emphasizes the separation of concerns between the client (handling user interface and experience) and the server (managing data storage and business logic). This separation allows each component to evolve independently, improving scalability.

  2. Uniform Interface: This simplifies the system by establishing a standardized way for web components to communicate. It includes four sub-constraints: resource identification, resource manipulation through representation, self-descriptive messages, and Hypermedia As The Engine Of Application State (HATEOAS). The uniformity provides a consistent interface across all services.

  3. Layered System: The architecture is composed of hierarchical layers, where each layer only interacts with the immediate layer below it. This enhances scalability, security, and allows for the addition of intermediate components like load balancers and proxy servers without affecting core functionality.

  4. Cacheable: Responses from the server must be explicitly labeled as cacheable or non-cacheable. Clients can then cache responses, which reduces server load, improves network efficiency, and enhances user experience through faster response times.

  5. Stateless: Each request from the client to the server must contain all information necessary to understand and process that request. The server does not store any client context between requests, meaning it won't remember your previous request. This significantly improves reliability, scalability, and visibility, as any server in a load-balanced setup can handle a request from the same client. (This concept was also covered in our HTTP video).

  6. Code on Demand (Optional): This optional constraint allows servers to temporarily extend client functionality by transferring executable code (like JavaScript) to the client. While it provides flexibility, it's not heavily used in REST architecture.

These six constraints were proposed by Roy Fielding to solve the web's scalability problem. Fielding later collaborated with Tim Berners-Lee, and together they worked on standardizing web designs, leading to the specification for HTTP 1.1, the first major standard version of the HTTP protocol.

In 2000, after the web's scalability crisis was averted, Roy Fielding formally named and described the web's architectural style in his PhD dissertation: REST (Representational State Transfer). Reading Fielding's original paper is highly recommended for backend engineers to gain deeper context on the origins of these patterns and standards.

Deconstructing "REST": Representational State Transfer

The name "REST" itself provides insight into its core principles:

  1. Representational: Resources (data or objects) on the web are represented in a specific format. These representations can vary based on client and server needs. Common formats include JSON (most popular, often for server-to-server communication), XML, and HTML (often for server-to-client browser communication). For example, a user resource might have different representations (e.g., JSON for an API client, HTML for a web browser) depending on the client's requirements.

  2. State: This refers to the current condition or attributes of a particular resource. Each resource has a state that can be transferred between the client and the server, driven by the resource's representation. For instance, a shopping cart's state would include items, quantities, and total price.

  3. Transfer: This indicates the movement of resource representations between the client and the server. Client-server communication involves exchanging these representations, typically using standard HTTP methods like GET, POST, PUT, DELETE, PATCH, OPTIONS, and HEAD. For example, a GET request for a web page transfers an HTML representation from the server to the client.

In summary, REST (Representational State Transfer) describes an architectural style where:

  • Resources are represented in different formats (e.g., JSON, HTML, XML).

  • The state of these resources can be transferred between the server and the client.

  • Clients and servers communicate by sharing these representations of a resource.

  • The entire system adheres to specific constraints to enhance scalability.

This historical and theoretical context helps understand the foundational principles behind RESTful APIs.

Designing API URLs / Routes

Let's now look at the practical aspects of designing API URLs, or routes.





Typical URL Structure

A standard website URL typically consists of:

  • Scheme: http:// or https:// (the secure, encrypted version).

  • Authority / Domain: E.g., srinously.xyz, which can also include a subdomain.

  • Resource / Path: The part indicating the specific resource being accessed. Forward slashes (/) represent a hierarchical relationship between resources.

  • Query Parameters: Used in GET APIs to pass key-value pairs for filtering, sorting, or other parameters (e.g., ?filter=active&sort=date).

  • Fragments: (Less common for APIs) Used to navigate to a particular section of a web page (e.g., #section-id).

Standard API URL Structure and Best Practices

When designing API URLs, particularly for RESTful APIs, certain industry standards and best practices are commonly followed:

  • Scheme: Always https:// for secure communication.

  • Subdomain: Often starts with api. (e.g., api.example.com) to clearly designate the API endpoint.

  • Versioning: Most APIs implement versioning, typically through routes (e.g., /v1/, /v2/) to manage changes without breaking older client integrations.

    • Example URL with versioning: https://api.example.com/v1/users

  • Resource Naming (Crucial Rule): Resources in the URI path segment should always be in the plural form.

    • For example, an API to list all books should be /api.example.com/v1/books.

    • Even when fetching a single book, the resource segment remains plural: /books/{book_id}. This is because the URI refers to the collection from which the specific resource is retrieved.

      • Good: GET /users (list all users)

      • Good: GET /users/123 (get user with ID 123)

      • Bad: GET /user (don't use singular)

      • Bad: GET /user/123 (still bad, even for single resource)

  • URL Readability and Conventions:

    • No Spaces or Underscores: Avoid spaces or underscores in URLs. These characters can cause issues in different environments.

    • Use Hyphens for Slugs: If a phrase needs to be included in the URL (like a human-readable identifier, or "slug"), replace spaces with hyphens. For example, "Harry Potter" becomes harry-potter.

      • Good: GET /books/harry-potter-and-the-sorcerers-stone

      • Bad: GET /books/harry_potter_and_the_sorcerers_stone

      • Bad: GET /books/Harry%20Potter%20and%20the%20Sorcerers%20Stone

    • Lowercase Slugs: Convert slugs to lowercase to avoid case mismatch problems across different operating systems and environments.

    • Hierarchical Relationship: The forward slash (/) indicates a hierarchical relationship. For instance, /books/harry-potter signifies that harry-potter is a specific resource within the books collection.

Idempotency and HTTP Methods

Idempotency is a very important theoretical concept with practical implications in REST API design.

Definition of Idempotency: The property of certain operations where performing the same action multiple times has the same effect as performing it once. In the context of REST APIs, this means that regardless of how many times a client performs a particular request, the outcome or side effect on the server environment remains the same.

We use various HTTP methods to handle different data operations. The five major methods are GET, POST, PUT, PATCH, and DELETE. (Other methods like HEAD and OPTIONS also exist but are used for specific purposes, such as fetching header information or handling CORS preflight requests).

Relating idempotency to these HTTP methods provides insights into when to use each one:

  • GET (Retrieve Data): The GET method is used to retrieve data from a server and is considered idempotent. Making multiple identical GET requests will always yield the same outcome from the server's perspective, as GET requests cause no change to the server's state. While the data might change due to other actions (e.g., another user creating a book), your GET request itself doesn't cause any side effects on the server.

    Bash
    # First GET request
    curl -X GET http://api.example.com/v1/users/123
    # Response: {"id": "123", "name": "Alice"}
    
    # Second GET request (same result, no server-side change caused by this request)
    curl -X GET http://api.example.com/v1/users/123
    # Response: {"id": "123", "name": "Alice"}
    
  • PUT (Complete Replacement): The PUT method is used to completely replace the representation of a resource on the server with the client's payload. For example, if updating a user object, a PUT request requires sending all fields (ID, name, created date) in the payload to replace the existing instance. PUT is idempotent because if you send the same payload multiple times, the resource will end up in the exact same state as if you sent it once.

    Bash
    # Assume user 123 exists: {"id": "123", "name": "Alice", "email": "alice@example.com"}
    
    # First PUT request to update user 123
    curl -X PUT http://api.example.com/v1/users/123 \
         -H "Content-Type: application/json" \
         -d '{"id": "123", "name": "Alice Smith", "email": "alice@example.com"}'
    # Response: 200 OK, {"id": "123", "name": "Alice Smith", "email": "alice@example.com"}
    
    # Second PUT request with the exact same payload (result is the same, no new side effects)
    curl -X PUT http://api.example.com/v1/users/123 \
         -H "Content-Type: application/json" \
         -d '{"id": "123", "name": "Alice Smith", "email": "alice@example.com"}'
    # Response: 200 OK, {"id": "123", "name": "Alice Smith", "email": "alice@example.com"}
    
  • PATCH (Partial Update): The PATCH method is used to partially update a resource on the server. For example, to update only the name of a user, you would send only the name field with its new value in the PATCH payload. PATCH is also considered idempotent because multiple identical PATCH requests (aimed at achieving a specific final state) should result in that same final state.

    • While PUT and PATCH are sometimes used interchangeably in internal APIs, it's crucial to stick to the standard for public APIs to avoid confusion for integrating developers. Use PATCH for partial updates and PUT for complete resource replacements.

    Bash
    # Assume user 123 exists: {"id": "123", "name": "Alice", "email": "alice@example.com"}
    
    # First PATCH request to update only the name of user 123
    curl -X PATCH http://api.example.com/v1/users/123 \
         -H "Content-Type: application/json" \
         -d '{"name": "Alice Cooper"}'
    # Response: 200 OK, {"id": "123", "name": "Alice Cooper", "email": "alice@example.com"}
    
    # Second PATCH request with the exact same payload (result is the same, user's name remains "Alice Cooper")
    curl -X PATCH http://api.example.com/v1/users/123 \
         -H "Content-Type: application/json" \
         -d '{"name": "Alice Cooper"}'
    # Response: 200 OK, {"id": "123", "name": "Alice Cooper", "email": "alice@example.com"}
    
  • DELETE (Resource Removal): The DELETE method is used to remove a resource. DELETE is also considered idempotent.

    • If you make a DELETE request for a user:

      • The first call successfully deletes the user.

      • Subsequent calls with the same request will find the user already deleted and typically return a 404 Not Found error. While an error is returned, no further side effect (change in server state caused by your repeated action) occurs after the first deletion. The resource remains deleted.

    Bash
    # Assume user 123 exists
    
    # First DELETE request
    curl -X DELETE http://api.example.com/v1/users/123
    # Response: 204 No Content (user 123 is now deleted)
    
    # Second DELETE request (user 123 no longer exists)
    curl -X DELETE http://api.example.com/v1/users/123
    # Response: 404 Not Found (no further change to server state caused by this request)
    
  • POST (Resource Creation / Custom Actions): The POST method is the only HTTP method considered non-idempotent.

    • When POST is used to create a new resource (e.g., a new book):

      • The first POST request creates a new book with a unique ID.

      • A second POST request with the exact same payload (e.g., same book name, description) will create another new book with a different unique ID. This means each POST call with the same payload results in a different side effect (a new resource being created).

    Bash
    # First POST request to create a new user
    curl -X POST http://api.example.com/v1/users \
         -H "Content-Type: application/json" \
         -d '{"name": "Bob", "email": "bob@example.com"}'
    # Response: 201 Created, {"id": "user-abc", "name": "Bob", "email": "bob@example.com", "createdAt": "..."}
    
    # Second POST request with the exact same payload (creates a *different* new user)
    curl -X POST http://api.example.com/v1/users \
         -H "Content-Type: application/json" \
         -d '{"name": "Bob", "email": "bob@example.com"}'
    # Response: 201 Created, {"id": "user-xyz", "name": "Bob", "email": "bob@example.com", "createdAt": "..."}
    
    • POST is also designed to be open-ended and used for non-CRUD or custom actions that don't fit the semantics of GET, PUT, PATCH, or DELETE.

      • For example, an API to "send email" (/send-email) doesn't fit a GET, PUT, PATCH, or DELETE operation. In such scenarios, POST is the appropriate method because it signifies an action being performed on the server that is not about simply creating, reading, updating, or deleting a specific named resource.

Practical API Interface Design: A Demo Walkthrough

Before writing any business logic, a backend engineer should prioritize designing the API interface. The interface should be intuitive, delightful to use, consistent, and adhere to REST API standards.

Benefits of Following Standards:

Following standards eliminates confusion, reduces assumptions, and minimizes human-related errors in your workflow. If an API doesn't follow standards (e.g., using POST for an update, or DELETE for a fetch), consumers of that API would have to either read the code (if accessible) or extensively experiment to understand its behavior. Adhering to common standards (even 80% adherence) significantly reduces the effort and time required for API integration, leading to fewer bugs, less confusion, and fewer sync-up calls between teams.

Starting Point: UI Design and Identifying Resources

The best place to start API design is from the UI design interface (e.g., Figma wireframes) or by thoroughly understanding user stories and requirements. By observing how end-users will interact with your platform's data, you gain a clear understanding of the relationship between user actions and the underlying data.

The first crucial step in REST API design is identifying resources. Resources are essentially nouns that you can find within your wireframes or by analyzing requirements.

For a project management SaaS platform (like Jira or Linear), examples of identified resources (nouns) would be:

  • Projects

  • Users

  • Organizations

  • Tasks

  • Tags

Once resources are identified, the typical workflow involves:

  1. Identifying all resources.

  2. Designing the database schema for these resources (this will be covered in the next video).

  3. Designing the API interface.

For this demonstration, we'll imagine our database schema is already designed and includes three core tables for a project management platform:

  • Organization: To track organizations.

  • Project: To track projects within an organization.

  • Task: To track tasks within a project.



After defining resources and imagining the schema, the next step is to identify the actions that a client needs to perform on these resources, typically beginning with standard CRUD operations (Create, Read, Update, Delete).

For the API interface design, we'll use Insomnia, a lightweight alternative to Postman, to visualize and test our API routes. The focus remains purely on the API's interface – how clients will interact with it – not the underlying server-side business logic or database interactions.

Demo: Designing Organization APIs

Let's design APIs for the Organization resource, covering common CRUD actions:

1. List All Organizations (GET /organizations)

  • HTTP Method: GET (for fetching data).

  • URL Structure (Demo): http://localhost:3000/organizations.

    • Note: In a production environment, this would typically include https:// and a version (e.g., /v1/).

  • Path Segment: organizations – it must be plural as per REST best practices for resource collections.

  • Expected Response (initial): An empty array [] if no organizations exist.

  • Success Status Code: 200 OK (indicating a successful fetch operation).

    Example Request & Response:

    Bash
    # Request to list all organizations
    curl -X GET http://localhost:3000/organizations
    
    JSON
    # Expected Response (if no organizations exist)
    {
        "data": [],
        "total": 0,
        "page": 1,
        "total_pages": 0
    }
    

    (Status: 200 OK)




2. Create Organization (POST /organizations)

  • HTTP Method: POST (for creating a new resource).

  • URL Structure (Demo): http://localhost:3000/organizations.

    • Notice the URL is the same as the GET for listing organizations. The server differentiates between these two API calls based on the HTTP method. A POST to this route triggers the creation logic.

  • Payload (Request Body): This will be a JSON object containing the data needed to create an organization. Fields like id, created_at, and updated_at are handled by the server/database and should be excluded from the client's payload.

  • Expected Response: The newly created organization entity, including its server-generated id and created_at timestamp.

  • Success Status Code: 201 Created (specifically for successful resource creation).

    Example Request & Response:

    Bash
    # Request to create an organization
    curl -X POST http://localhost:3000/organizations \
         -H "Content-Type: application/json" \
         -d '{
               "name": "Acme Corp",
               "status": "active",
               "description": "A leading technology company."
             }'
    
    JSON
    # Expected Response
    {
        "id": "org-xyz-1",
        "name": "Acme Corp",
        "status": "active",
        "description": "A leading technology company.",
        "createdAt": "2025-07-12T10:00:00Z",
        "updatedAt": "2025-07-12T10:00:00Z"
    }
    

    (Status: 201 Created)




Understanding Pagination in List APIs

After creating organizations using the POST API, a subsequent GET /organizations call will now return the created entities. You'll notice the response for list APIs often includes more than just the data array; it includes pagination details.




Why Pagination is Essential:

Pagination is a technique used on the server side to return a portion of a list of resources in a response, rather than all of them. This is critical for several reasons:

  • Performance: Sending a very large number of resources (e.g., thousands of organizations) in a single response leads to heavy JSON serialization and deserialization operations, causing significant delays and performance impacts on both the server and client.

  • User Experience: Large response times translate to perceived delays for the end-user. Even if thousands of items exist, a user typically only sees 10-20 at a time on their screen. Pagination allows for fetching only what's immediately needed.

  • Network Efficiency: Reduces the amount of data transferred over the network.

How Pagination Works:

  • In the initial API call, a small, relevant portion of data (e.g., the latest 10 organizations) is returned.

  • If the user wishes to see more (e.g., clicks "next page" or scrolls infinitely), the client makes another API call requesting the subsequent portion of data.

Typical Paginated Response Fields:

A well-designed paginated response typically includes the following fields:

  • data: An array containing the actual resources for the current requested portion or "page".

  • total: The total count of all resources existing in the database (e.g., 50 organizations), regardless of how many are on the current page. This is useful for UI elements like "Viewing 10 of 50".

  • page: The number of the current page being returned by the server.

  • total_pages: The total number of pages available for all resources, given the specified limit. This helps the frontend determine if there are more pages to fetch.

Client-Side Control via Query Parameters:

Clients control pagination using query parameters in their GET requests:

  • limit: Specifies the number of resources desired per page (e.g., ?limit=2). The server should provide a default value if this parameter is not sent by the client.

  • page: Specifies the particular page number to retrieve (e.g., ?page=1). The server typically defaults to page 1 if not provided.

Pagination Demo Example (with 5 organizations, limit=2):

Let's assume we have 5 organizations created: Org 5 (latest), Org 4, Org 3, Org 2, Org 1 (oldest).

  • Request: GET http://localhost:3000/organizations?limit=2&page=1

    Bash
    curl -X GET "http://localhost:3000/organizations?limit=2&page=1"
    
    • Response: Returns the first 2 organizations (Org 5, Org 4, sorted by created_at descending).

      JSON
      {
          "data": [
              {"id": "org-5", "name": "Org 5", ...},
              {"id": "org-4", "name": "Org 4", ...}
          ],
          "total": 5,
          "page": 1,
          "total_pages": 3
      }
      

      (Status: 200 OK)




  • Request: GET http://localhost:3000/organizations?limit=2&page=2

    Bash
    curl -X GET "http://localhost:3000/organizations?limit=2&page=2"
    
    • Response: Returns the next 2 organizations (Org 3, Org 2).

      JSON
      {
          "data": [
              {"id": "org-3", "name": "Org 3", ...},
              {"id": "org-2", "name": "Org 2", ...}
          ],
          "total": 5,
          "page": 2,
          "total_pages": 3
      }
      

      (Status: 200 OK)



  • Request: GET http://localhost:3000/organizations?limit=2&page=3

    Bash
    curl -X GET "http://localhost:3000/organizations?limit=2&page=3"
    
    • Response: Returns the last organization (Org 1).

      JSON
      {
          "data": [
              {"id": "org-1", "name": "Org 1", ...}
          ],
          "total": 5,
          "page": 3,
          "total_pages": 3
      }
      

      (Status: 200 OK)




This mechanism ensures efficient data transfer and a smooth user experience.

Default Parameters for List APIs

Even if the client doesn't send page or limit parameters, the server should set sensible defaults (e.g., page=1, limit=10 or 20) to ensure a consistent response. This prevents validation errors and improves the developer experience.

Sorting in List APIs

List APIs should also support sorting.

  • Query Parameters:

    • sort_by: The field by which to sort (e.g., ?sort_by=name).

    • sort_order: The order of sorting (ascending or descending).

  • Default Sorting: The server should always apply a default sort order, typically by created_at in descending order (to show the latest entries first). If no sort_order is provided by the client, descending should be the default. This ensures consistent ordering of data across requests, as databases do not inherently store entries in a specific sequence.

  • Example (assuming Org 5, Org 4, Org 3, Org 2, Org 1 were created in that order):

    • Request: GET http://localhost:3000/organizations?sort_by=name

      • (Default sort_order is descending and sort_by for names results in Org 5, Org 4, ..., Org 1)



    Bash
    curl -X GET "http://localhost:3000/organizations?sort_by=name"
    
    • Request: GET http://localhost:3000/organizations?sort_by=name&sort_order=ascending

      • Response: Would return organizations sorted alphabetically by name: Org 1, Org 2, Org 3, Org 4, Org 5.

    Bash
    curl -X GET "http://localhost:3000/organizations?sort_by=name&sort_order=ascending"
    

Filtering in List APIs

List APIs should also support filtering.

  • Query Parameters: Filter parameters are typically named after the field they are filtering (e.g., ?status=active or ?name=Org%204).



  • Example (assuming we create an "Org 6" with status "archived"):

    Bash
    # Create Org 6 as archived
    curl -X POST http://localhost:3000/organizations \
         -H "Content-Type: application/json" \
         -d '{"name": "Org 6", "status": "archived"}'
    
    • Request: GET http://localhost:3000/organizations?status=archived

      Bash
      curl -X GET "http://localhost:3000/organizations?status=archived"
      
      • Response: Returns only organizations with an "archived" status (e.g., Org 6).

      JSON
      {
          "data": [{"id": "org-6", "name": "Org 6", "status": "archived", ...}],
          "total": 1, "page": 1, "total_pages": 1
      }
      

      (Status: 200 OK)

    • Request: GET http://localhost:3000/organizations?status=active

      Bash
      curl -X GET "http://localhost:3000/organizations?status=active"
      
      • Response: Returns only organizations with an "active" status (e.g., Org 1, Org 2, Org 3, Org 4, Org 5).

    • Request: GET http://localhost:3000/organizations?name=Org%204

      Bash
      curl -X GET "http://localhost:3000/organizations?name=Org%204"
      
      • Response: Returns only the organization named "Org 4".

  • No Data for Filter: If a filter yields no results (e.g., GET /organizations?status=random_value), the API should return an empty data array with a 200 OK status code, not a 404 Not Found. A 404 is reserved for requests for a specific resource that does not exist.

    • Example:

      • Request: GET http://localhost:3000/organizations?status=non_existent_status

      Bash
      curl -X GET "http://localhost:3000/organizations?status=non_existent_status"
      
      • Response:

      JSON
      {
          "data": [],
          "total": 0,
          "page": 1,
          "total_pages": 0
      }
      

      (Status: 200 OK)

3. Update Organization (PATCH /organizations/{id})

  • HTTP Method: PATCH (for partial updates).

    • While PUT can also be used for updates, PATCH is generally preferred for partial updates as it semantically conveys that only specific fields are being modified, not the entire resource being replaced.

  • URL Structure: http://localhost:3000/organizations/{organization_id}.

    • The {organization_id} is a dynamic parameter indicating which specific organization to update.



  • Payload (Request Body): A JSON object containing only the fields to be updated.

  • Expected Response: The updated organization entity.

  • Success Status Code: 200 OK (for a successful update)

    Example Request & Response (assuming org-6 is archived, changing its status):

    Bash
    # Request to update organization status
    curl -X PATCH http://localhost:3000/organizations/org-6 \
         -H "Content-Type: application/json" \
         -d '{"status": "active"}'
    
    JSON
    # Expected Response
    {
        "id": "org-6",
        "name": "Org 6",
        "status": "active",
        "description": null,
        "createdAt": "2025-07-12T10:05:00Z",
        "updatedAt": "2025-07-12T10:15:00Z" # Updated timestamp
    }
    

    (Status: 200 OK).

4. Get Single Organization (GET /organizations/{id})

  • HTTP Method: GET (for fetching a single resource).

  • URL Structure: http://localhost:3000/organizations/{organization_id}.

    • The route structure for fetching, updating, and deleting a single resource is typically similar, with the HTTP method differentiating the action.

  • Expected Response: The specific organization entity requested.

  • Success Status Code: 200 OK.

  • Error Status Code: 404 Not Found if the requested organization_id does not exist.

    Example Request & Response:

    Bash
    # Request to get a single organization
    curl -X GET http://localhost:3000/organizations/org-xyz-1
    
    JSON
    # Expected Response (if found)
    {
        "id": "org-xyz-1",
        "name": "Acme Corp",
        "status": "active",
        "description": "A leading technology company.",
        "createdAt": "2025-07-12T10:00:00Z",
        "updatedAt": "2025-07-12T10:00:00Z"
    }
    

    (Status: 200 OK)

    Bash
    # Request for a non-existent organization
    curl -X GET http://localhost:3000/organizations/non-existent-id
    
    JSON
    # Expected Response (if not found)
    {
        "error": "Organization not found",
        "code": "ORGANIZATION_NOT_FOUND"
    }
    

    (Status: 404 Not Found)

5. Delete Organization (DELETE /organizations/{id})

  • HTTP Method: DELETE (for removing a resource).

  • URL Structure: http://localhost:3000/organizations/{organization_id}.

  • Expected Response: An empty response body.

  • Success Status Code: 204 No Content (indicates successful operation with no content to return).

  • Error Status Code: 404 Not Found if the organization_id does not exist (as discussed in idempotency, if attempted again after successful deletion).

    Example Request:

    Bash
    # Request to delete an organization
    curl -X DELETE http://localhost:3000/organizations/org-xyz-1
    

    (Status: 204 No Content, with an empty response body)

6. Custom Action: Archive Organization (POST /organizations/{id}/archive)

  • Scenario: Archiving an organization might involve more than just changing a status field; it could trigger cascading operations like deleting associated projects/tasks, sending notifications, etc. This makes it a custom action, not a simple update.

  • HTTP Method: POST (as it's a non-CRUD custom action).

  • URL Structure: http://localhost:3000/organizations/{organization_id}/archive.

    • This follows a clear hierarchical path: resource_collection / specific_resource / action.

  • Payload: May or may not require a body, depending on the action's needs. For archiving, it might be an empty body or contain specific parameters.

  • Expected Response: The updated organization entity (with its status changed to "archived").



  • Success Status Code: 200 OK (as no new resource is created, unlike a typical POST). This highlights that POST requests for custom actions don't always return 201 Created.

    Example Request & Response (assuming org-xyz-1 is active, archiving it):

    Bash
    # Request to archive an organization (no request body needed for this specific action)
    curl -X POST http://localhost:3000/organizations/org-xyz-1/archive
    
    JSON
    # Expected Response
    {
        "id": "org-xyz-1",
        "name": "Acme Corp",
        "status": "archived", # Status changed
        "description": "A leading technology company.",
        "createdAt": "2025-07-12T10:00:00Z",
        "updatedAt": "2025-07-12T10:20:00Z" # Updated timestamp
    }
    

    (Status: 200 OK)

Demo: Designing Project APIs

Now, let's apply these patterns to design APIs for the Project resource. We'll organize these endpoints in a separate folder in Insomnia for better management.

1. Create Project (POST /projects)

  • HTTP Method: POST.

  • URL Structure: http://localhost:3000/projects.

    • Follows the plural resource naming convention.

  • Payload (Request Body): JSON payload for project creation.

    • Fields like id, created_at, updated_at are server-handled.

  • JSON Payload Consistency (Camel Case): All JSON fields in both requests (client to server) and responses (server to client) should consistently use camelCase (e.g., organizationId, createdAt, updatedAt). This is a common standard for JSON and reduces guesswork for clients.

  • Success Status Code: 201 Created.

    Example Request & Response:

    Bash
    # Request to create a project
    curl -X POST http://localhost:3000/projects \
         -H "Content-Type: application/json" \
         -d '{
               "name": "Website Redesign",
               "organizationId": "org-xyz-1",
               "status": "inProgress",
               "description": "Redesign the company website for better UX."
             }'
    
    JSON
    # Expected Response
    {
        "id": "proj-a1b2",
        "name": "Website Redesign",
        "organizationId": "org-xyz-1",
        "status": "inProgress",
        "description": "Redesign the company website for better UX.",
        "createdAt": "2025-07-12T10:30:00Z",
        "updatedAt": "2025-07-12T10:30:00Z"
    }
    

    (Status: 201 Created)

2. List All Projects (GET /projects)

  • HTTP Method: GET.

  • URL Structure: http://localhost:3000/projects.

    • Similar route to POST /projects, differentiated by HTTP method.

  • Pagination, Sorting, Filtering: This endpoint should also support limit, page, sort_by, sort_order, and various filter parameters, following the same patterns established for the Organization list API.

    Example (Listing projects with filters and pagination):

    Bash
    # Request to list projects for a specific organization, active, and paginated
    curl -X GET "http://localhost:3000/projects?organizationId=org-xyz-1&status=inProgress&limit=10&page=1"
    
    JSON
    # Expected Response (example)
    {
        "data": [
            {"id": "proj-a1b2", "name": "Website Redesign", "status": "inProgress", ...},
            {"id": "proj-c3d4", "name": "Mobile App Dev", "status": "inProgress", ...}
        ],
        "total": 2,
        "page": 1,
        "total_pages": 1
    }
    

    (Status: 200 OK)

Importance of Consistency Across Resources:

When designing APIs for a single platform, it is crucial to maintain consistency across different resources (e.g., Organization and Project).

  • Payload Consistency: If Organization APIs use description as a field key, Project APIs should also use description for the same concept, not desc or DSC.

  • Route Consistency: If Organization APIs use plural resource names (/organizations), Project APIs should also use plural names (/projects), not /project.

This consistency reduces the learning curve for frontend engineers and other clients integrating your API. They can make informed assumptions based on one API's design, saving time and effort, and preventing unnecessary errors and debugging. A good backend engineer prioritizes this consistency in styling across all API designs.

3. Get Single Project (GET /projects/{id})

  • HTTP Method: GET.

  • URL Structure: http://localhost:3000/projects/{project_id}.

    • Similar to GET /organizations/{id}.

  • Success Status Code: 200 OK.

    Example Request:

    Bash
    curl -X GET http://localhost:3000/projects/proj-a1b2
    

    Expected Response (similar to create project, but for specific ID):

    JSON
    {
        "id": "proj-a1b2",
        "name": "Website Redesign",
        "organizationId": "org-xyz-1",
        "status": "inProgress",
        "description": "Redesign the company website for better UX.",
        "createdAt": "2025-07-12T10:30:00Z",
        "updatedAt": "2025-07-12T10:30:00Z"
    }
    

    (Status: 200 OK)

4. Update Project (PATCH /projects/{id})

  • HTTP Method: PATCH (for partial updates).

  • URL Structure: http://localhost:3000/projects/{project_id}.

    • Similar to PATCH /organizations/{id}.

  • Payload (Request Body): JSON containing fields to update.

  • Success Status Code: 200 OK.

    Example Request & Response:

    Bash
    # Request to update a project's status
    curl -X PATCH http://localhost:3000/projects/proj-a1b2 \
         -H "Content-Type: application/json" \
         -d '{"status": "completed"}'
    
    JSON
    # Expected Response
    {
        "id": "proj-a1b2",
        "name": "Website Redesign",
        "organizationId": "org-xyz-1",
        "status": "completed", # Status changed
        "description": "Redesign the company website for better UX.",
        "createdAt": "2025-07-12T10:30:00Z",
        "updatedAt": "2025-07-12T10:45:00Z" # Updated timestamp
    }
    

    (Status: 200 OK)

5. Delete Project (DELETE /projects/{id})

  • HTTP Method: DELETE.

  • URL Structure: http://localhost:3000/projects/{project_id}.

    • Similar to DELETE /organizations/{id}.

  • Expected Response: An empty response body.

  • Success Status Code: 204 No Content.

    Example Request:

    Bash
    # Request to delete a project
    curl -X DELETE http://localhost:3000/projects/proj-a1b2
    

    (Status: 204 No Content, with an empty response body)

6. Custom Action: Clone Project (POST /projects/{id}/clone)

  • Scenario: Cloning a project means creating a new project with the same values, but it might also involve cloning associated tasks, sending emails to the project owner, and other server-side operations that cannot be replicated by a simple POST /projects call.

  • HTTP Method: POST (as it's a non-CRUD custom action).

  • URL Structure: http://localhost:3000/projects/{project_id}/clone.

    • This follows the resource_collection / specific_resource / action pattern.

  • Payload: May not require a body for a basic clone, but could accept fields for overriding values in the cloned project.

  • Expected Response: The newly created, cloned project entity.

  • Success Status Code: 201 Created. This differs from the Archive Organization custom action because cloning creates a new resource on the server.

    Example Request & Response (cloning proj-a1b2):

    Bash
    # Request to clone a project (no body needed for a direct clone)
    curl -X POST http://localhost:3000/projects/proj-a1b2/clone
    
    JSON
    # Expected Response (a new project ID is generated, with similar data to original)
    {
        "id": "proj-e5f6", # New unique ID
        "name": "Website Redesign (Cloned)", # Name might be slightly modified
        "organizationId": "org-xyz-1",
        "status": "inProgress",
        "description": "Redesign the company website for better UX.",
        "createdAt": "2025-07-12T10:50:00Z", # New creation timestamp
        "updatedAt": "2025-07-12T10:50:00Z"
    }
    

    (Status: 201 Created)

By now, you should be able to establish and follow consistent patterns for designing APIs for any resource, whether it's Task or any other entity. The foundational principles remain the same.

Best Practices for Delightful API Design

Beyond the basic CRUD and custom action patterns, here are crucial considerations for providing a great experience for API consumers and maintainers:

  1. Provide Interactive Documentation (e.g., Swagger/OpenAPI):

    • Integrate tools like Swagger (OpenAPI) from the initial planning stages.

    • This provides an interactive playground for trying out your APIs and serves as live documentation.

    • Regularly create, maintain, and update your OpenAPI or Swagger documentation. This is a hallmark of a good backend engineer.

  2. Make APIs Intuitive and Consistent:

    • All your routes, dynamic parameters, custom actions, and JSON payloads should follow a single, consistent pattern.

    • Strive to adhere to global REST API standards. If for some reason you deviate, establish your own internal standard and stick to it rigidly across all endpoints and resources.

    • Inconsistency is a huge pain point for integrators. Maintain consistent behavior, data formats, and naming conventions.

  3. Provide Sensible Defaults:

    • For GET (List) APIs:

      • If the client does not specify page, default to page=1.

      • If limit is not passed, set a default limit (e.g., 10 or 20).

      • If sort_by is not passed, default to a sensible field like created_at.

      • If sort_order is not passed, default to descending.

    • For POST (Create) APIs:

      • Only require the absolute minimum information needed to create an entity.

      • Provide sensible defaults for optional fields. For example, when creating an organization, if status is not provided by the client, the server can default it to "active" based on business logic and common sense. This allows clients to create resources with minimal payload.

        • Example (Pseudo-code for server-side handling):

          Python
          def create_organization(payload):
              # Minimum required field
              if "name" not in payload:
                  raise ValueError("Organization name is required")
          
              new_org = {
                  "id": generate_unique_id(),
                  "name": payload["name"],
                  "description": payload.get("description", None), # Optional, defaults to None if not provided
                  "status": payload.get("status", "active"), # Sensible default for status
                  "createdAt": current_timestamp(),
                  "updatedAt": current_timestamp()
              }
              # Save new_org to database
              return new_org
          
        • Example Call: For POST /organizations with payload {"name": "New Org"} and no description or status provided by client, the server might still successfully create it with status: "active" and description: null (if optional).

  4. Always Avoid Abbreviations:

    • Do not use abbreviations (e.g., desc for description, orgId for organizationId) in your field names or query parameters.

    • The information available to API designers is not the same as that available to API integrators. Avoid making them guess the meaning of abbreviated fields.

    • Keep your fields intuitive, readable, and fully spelled out.

      • Good:

        JSON
        {"projectId": "proj-123", "description": "This is a detailed description."}
        
      • Bad:

        JSON
        {"projId": "proj-123", "desc": "Short desc."}
        

These are fundamental principles to keep in mind when designing API interfaces.

Design First: The Backend Engineer's Responsibility

As a backend engineer, your responsibility is to provide delightful and intuitive APIs. Always remember that a REST API is first designed, not immediately coded or programmed.

Before jumping into programming (regardless of the language or framework), dedicate a separate session to just designing your API interface. Using API clients like Insomnia or Postman, or OpenAPI playgrounds like Swagger, to design the interface will give you crucial insights into how clients and consumers will interact with your APIs. This "design first" approach will help you create much better and more intuitive APIs.

This blog focused purely on designing the API interface, without delving into programming language or framework-specific implementations. Hopefully, with these guidelines, you'll be able to create delightful and intuitive APIs from now on!


 

Comments

Popular posts from this blog

The Anatomy of a Backend Request: Layers, Middleware, and Context Explained

JS - Note

Validations and Transformations (Sriniously)