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
orPUT
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:
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.
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.
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.
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.
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).
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:
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.
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.
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://
orhttps://
(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 thatharry-potter
is a specific resource within thebooks
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): TheGET
method is used to retrieve data from a server and is considered idempotent. Making multiple identicalGET
requests will always yield the same outcome from the server's perspective, asGET
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), yourGET
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): ThePUT
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, aPUT
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): ThePATCH
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 thePATCH
payload.PATCH
is also considered idempotent because multiple identicalPATCH
requests (aimed at achieving a specific final state) should result in that same final state.While
PUT
andPATCH
are sometimes used interchangeably in internal APIs, it's crucial to stick to the standard for public APIs to avoid confusion for integrating developers. UsePATCH
for partial updates andPUT
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): TheDELETE
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): ThePOST
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 eachPOST
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 ofGET
,PUT
,PATCH
, orDELETE
.For example, an API to "send email" (
/send-email
) doesn't fit aGET
,PUT
,PATCH
, orDELETE
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:
Identifying all resources.
Designing the database schema for these resources (this will be covered in the next video).
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. APOST
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
, andupdated_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
andcreated_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 specifiedlimit
. 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
Bashcurl -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
Bashcurl -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
Bashcurl -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
ordescending
).
Default Sorting: The server should always apply a default sort order, typically by
created_at
indescending
order (to show the latest entries first). If nosort_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
isdescending
andsort_by
for names results inOrg 5, Org 4, ..., Org 1
)
Bashcurl -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
.
Bashcurl -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
Bashcurl -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
Bashcurl -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
Bashcurl -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 emptydata
array with a200 OK
status code, not a404 Not Found
. A404
is reserved for requests for a specific resource that does not exist.Example:
Request:
GET http://localhost:3000/organizations?status=non_existent_status
Bashcurl -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 requestedorganization_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 theorganization_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 typicalPOST
). This highlights thatPOST
requests for custom actions don't always return201 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 theOrganization
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 usedescription
as a field key,Project
APIs should also usedescription
for the same concept, notdesc
orDSC
.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:
Bashcurl -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 theArchive 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:
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.
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.
Provide Sensible Defaults:
For
GET
(List) APIs:If the client does not specify
page
, default topage=1
.If
limit
is not passed, set a defaultlimit
(e.g., 10 or 20).If
sort_by
is not passed, default to a sensible field likecreated_at
.If
sort_order
is not passed, default todescending
.
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):
Pythondef 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 nodescription
orstatus
provided by client, the server might still successfully create it withstatus: "active"
anddescription: null
(if optional).
Always Avoid Abbreviations:
Do not use abbreviations (e.g.,
desc
fordescription
,orgId
fororganizationId
) 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
Post a Comment