RESTful API design guide

Table of Content

Concept

Base things in Rest API

Rest API stands for Representational State Transfer.

Basic things in Rest API design:

  1. Resource – an object which has an identifier.
  2. Method – an operation indicates what kind of state transfer will happen.
  3. State – state is a current representation of a resource, it means that our resource can have different data at a different time, and it is a state of the resource.

In Rest API resources are identified by their URLs, so it means that 1 unique URL = 1 unique resource.

We have Server and Clients, we can transfer the state of resource from server to client or vice versa.

What does state transfer mean?

Let’s say we want to create a new user. How we can understand this basic operation from a state transfer perspective?
Creating a new user means that, our client has a new state (new user), and our client needs to transfer its new state about user resource to the server.
So, we can say that creating a new user is transferring a new user state to the server.

After we transfer the state of the resource to the server or vice versa, both parties should have the same state if an external factor does not cause any change.

It means that if we updated a resource, and we called GET method to get the same resource, we should see the same data as we sent a request to update it.

Is REST API stateless or stateful?

REST APIs are stateless because, rather than relying on the server remembering previous requests, REST applications require each request to contain all of the information necessary for the server to understand it.

Don’t write as Restful, Think as Restful

Restful service should be abstract, not a representation of the backend service layer as is.

You need to think about restful service independently, don’t try to implement Rest APIs just like converting service layer codes to APIs, instead think from an API consumer perspective.

Rest APIs consist of resources and CRUD operations on these resources

Try to be proactive, if there are some struggling points on how you can design Rest API for a specific case, just think about how it can be translated to crud operation, and how consumers can easily understand the design.

Use nouns instead of verbs in endpoint paths

Incorrect variant:

GET /getUserById/15

This endpoint is wrong if we take into consideration that in Rest APIs endpoints (paths) are resource identifiers, so /get-users/15 as an identifier does not make any sense

Correct variant:

GET /users/15

Now it makes sense, GET the resource whose identifier is /users/15

Use hyphens (-) to improve the readability of URIs

Incorrect variant:

GET /projectGroups/6

Also, please do not use underscores ( _ ) for that:

GET /project_groups/6

Correct variant:

GET /project-groups/6

It is more neat and readable.

Name collections with plural nouns

We should name our collections as plural.

GET /users <- we can think this as we are getting list of users.
GET /users/1 <- we can think this as we are getting user number one from list of users(previous collection resource).

If we use singular names, it will be confusing.

GET /user <- it may be misunderstood as we are getting single user.
GET /user/1 <- it may be misunderstood as we are getting user’s sub resource named 1.

Examples:

GET /users
GET /users/1
GET /users/1/orders

If a URL parameter stands for an entity id then we should name this parameter not just {id} but like {entityId}. And if for an instance we have a URL like this:

GET /users/2/teams/3

then we should arrange the URL as:

GET /users/{userId}/teams/{teamId}

And just to keep everything unified, even if there is only one id-parameter, let’s call it the same way anyway:

GET /users/{userId}

Map Correct HTTP Methods to REST Operations

Method Usage Example
GET Get Resource GET /users GET /users/1 GET /users/1/star
POST Create Resource POST /users POST /users/1/star
PUT REPLACE/Full Update PUT /users/15 {full body}
PATCH Partial Update PATCH /users/15 {partial body}
DELETE Delete Resource PATCH /users/15

Nesting resources for hierarchical objects

Avoid creating a nesting resource if a sub-resource can be used separately.

In general, using nested resources isn’t as flexible as using root resources only.

Redundant endpoints also increase the surface of our API, and while more readable URLs for our resource relationships are a good thing for developer experience, a giant amount of endpoints is not.

Multiple endpoints increase the effort for the API owner to document the whole thing and make onboarding for new customers much more troublesome.

Multiple endpoints that return the same representations can also lead to problems with caching and can violate one of the core principle, the Uniform Interface.

Desired approach:

GET /orders?userId=1    <- not nested resource, filtered by userId

Not desired approach:

GET /users/1/orders     <- nested resource

When to use a nested resource?

  1. If a nested resource can be created/updated/queried only if you have information about its parent.
  2. Sub-resource is logically related to its parent.

Convert Actions to Resources or Fields

Let’s say we have a user resource, and we want to enable/disable, we want to give a star to this user

Simple but not desired:

POST /users/15/add-star
POST /users/15/remove-star
POST /users/15/activate
POST /users/15/deactivate

What instead can we do?

Preferable approach: Converting actions to fields

In this solution we think that activated and star are not sub resource, they are properties of our user

GET /users/15 RESPONSE: {...activated: true}
PATCH /users/15 REQUEST BODY: {activated: true}

Possible approach: Converting actions to sub-resources

We can think as our user resource has a sub resource named activated (adverb form of active) and if we create this sub resource we say that we activate the user, if we remove this sub resource we say that we deactivate the user

GET /users/15/activated    <- 200 -> activated, 404 -> dectivated
POST /users/15/activated   <- this will activate the user
DELETE /users/15/activated <- this will deactivate the user

Same idea for starring

POST /users/15/star    <- this will star the user
DELETE /users/15/star  <- this will unstar the user

Basically what we’re trying to do is to make our actions look like Rest API, so translate actions to resources. We can use this approach if some logic cannot be expressed via the preferable approach, such as user promotion or status changing, etc, but only in the case if a better server-side solution cannot be found.

Use Cases for Custom Methods

Some additional scenarios where custom methods may be the right choice:

  • Restart an operation. Restart is a well-known concept that can translate well to a custom method which intuitively meets developer expectations.
  • Send mail. Creating an email message should not necessarily send it (draft). Compared to the design alternative (move a message to an "Outbox" collection) custom method has the advantage of being more discoverable by the API user and models the concept more directly.
  • Promote an employee. If implemented as a standard update, the client would have to replicate the corporate policies governing the promotion process to ensure the promotion happens to the correct level, within the same career ladder etc.
  • Batch methods. For performance-critical methods, it may be useful to provide custom batch methods to reduce per-request overhead.

A few examples where a standard method is a better fit than a custom method:

  • Query resources with different query parameters (use standard list method with standard list filtering).
  • Simple resource property change (use standard update method with field mask).
  • Dismiss a notification (use standard delete method).

Common Custom Methods

The curated list of commonly used or useful custom method names is below. API designers should consider these names before introducing their own to facilitate consistency across APIs.

Method Name Custom verb HTTP verb Note
Cancel :cancel POST Cancel an outstanding operation.
Move :move POST Move a resource from one parent to another, such as folders.move.
Search :search GET Alternative to List for fetching data that does not adhere to List semantics.
Undelete :undelete POST Restore a resource that was previously deleted. The recommended retention period is 30-day.

Handle errors gracefully and return standard error codes

HTTP status as Error code

In rest API we should use HTTP status codes to describe what the problem is about

Main Idea for status codes:

  • 2** (mainly 200 is used) status codes are for describing that an operation is succeeded
  • 3** redirection happened, the resource which you are trying to operate is moved to another resource identifier or moved to another server
  • 4** client data are incorrect, client sent something wrong, need to fix it
  • 5** server cannot process the problem, 5xx errors indicate that the problem cannot be fixed from a client perspective and something corrupted server

In an ideal world, 5** error should not happen, if we are returning 5** status codes, it means that we have some fault in the application code or in our infrastructure.

Let’s go to detailed status codes (most used error codes by REST APIs)

Status Code Message Rest API Usage
2**
200 OK Updating Resource with PUT or PATCH method
201 CREATED Creating Resource with POST Method
204 NO_CONTENT Resource deleted, as we not returning any data in DELETE, it is better to indicate that there are no content
4**
400 BAD_REQUEST POST/PUT/PATCH, why creating/updating request, request input validation failed, client sent data which is wrong from servers perspective
401 UNAUTHENTICATED User is not authenticated and this resource needs user identification (you need to login or send access token, etc.)
403 FORBIDDEN User is identified but don’t have access to this resource or cannot call this method in this access because of insufficient permission
404 NOT_FOUND Resource not found, we should return 404 if the resource we are operating on not found
405 METHOD_NOT_ALLOWED Resource not found, we should return 404 if the resource we are operating on not found
5**
500 INTERNAL_ERROR We should use that if we have some found on application codes

Notes: you should not use 404 if the resource which is you are accessing in the backend logic not found, 404 is for when consumer try to do some operation on resource and that resource not found.

Example:

POST /restaurants
{
   "user": {
      *"id": 13  <- we don't have a user with id=13*
   }
}

In this case, we should not return 404 as the resource we are operating on is restaurants, what we could not find is a different resource. Instead, we may use 400 Bad request with a message: User not found with id=13.

Error message

Besides, for status codes we should generalize error response, we should return structured data for describing what went wrong. For 400 errors it is better to show field errors alongside with field names like:

{
   "message" : "name, age fields rejected",
   "rejectedFields": [
      {
         "field": "name",
         "message": "must not be blank"
      },
      {
         "field": "age",
         "message": "must be between 0 and 150"
      }
   ]
}

Allow filtering, sorting, and pagination

It is not important to handle filtering, sorting, pagination for all resources, these requirements should meet consumer needs. But it is better to handle filtering, sorting and pagination for resources, and it is required to do it consistently

Filtering

Filtering can be handled by two approaches:

  1. Boolean search: /users?age=13
    Using filtering by query parameters you can support multiple and multichoice filtering options like users can be searched by age, name, or both.
  2. Advanced query filtering: /users?filters={"groupOp":"AND", "rules":[{ "field": "user.id", "op": "eq", "data": "13" }]}
    This approach allows to apply any filter structures, group filters and apply different condition operations AND or OR.

Sorting

GET  /users?sortColumns=age,name&sortDirections=DESC,ASC

Pagination

GET  /users?currentPage=1&itemsPerPage=10

Aggregation in Rest API

It is a little hard to create a better API for resource aggregation, but we have some solutions for that:

Let’s say we have users resource collection (GET /api/1.0/users).
How we can get resource count and average age by country?

Create specialized sub resource

In this solution, we just create a new sub resource directly from the collection resource without item resources and separate an aggregate sub resource from the resource itself by “/:“ (trailing slash and colon) keyword. This means that a response is based on the data of the resource but has a different structure.

Formula:

GET /api/1.0/{resource-name}/:{aggregate-name}

Example:

GET /api/1.0/users/:salaries
Response:
[
  {
    "user": {"id": 13},
    "salaries": {"2020-10": 2500, "2021-04": 2700}
  },
  {
    "user": {"id": 16},
    "salaries": {"2020-10": 2500, "2021-04": 3000, "2021-10": 3200}
  }
]

At this moment, for simplifying API resources we don’t consider implementation of applying complex aggregations on a client side. In the future, if we decided to implement it the next resources would be helpful: one and two.

Bulk Operations in Rest API

Sometimes it is required to do bulk operations to increase performance

Let’s check some bulk operations in the Rest API world:

Remove sub-resources of resource OR Remove everything in the collection

DELETE /users <- this will cause to delete the entire collection, to delete all its items.

Bulk create resources and/or update

PATCH /users <- this will check all sent items one by one, and if the resource exists it will update it, if not create it.

Bulk replace resources

PUT /users <- this will update all users to the given state (given in the request body) and remove everything else like replacing the resource with given.

API versioning

API versioning and handling versions with caution is a must in the Rest API world.
Example for a version:

/api/v1/users

If we go production with version 1, and we need to do some breaking changes to the version 1 (which is affecting Rest API design) we need to introduce a new version:

/api/v1.1/users

File Uploading

Basically there are three choices:

  1. base64 – encode the file, at the expense of increasing the data size by around 33%, and add processing overhead in both the server and the client for encoding/decoding.
  2. send the file first in a multipart/form-data POST, and return an ID to the client. The client then sends the metadata with the ID, and the server re-associates the file and the metadata.
  3. send the metadata first, and return an ID to the client. The client then sends the file with the ID, and the server re-associates the file and the metadata.

Use the first approach, base64, if most of the files in your application are less than 1Mb. This approach is as RESTful service as possible. You send metadata and a file in one request and don’t bother about data consistency. The server processes the data in one approach.
Example:

POST  /users
{
    "name": "John Smith",
    "image": "data:/png;base64,iVBORw0KGgoAAA...",
    "cv": "data:/pdf;base64,JVBERi0xLjQKJfbk..."
}

There is a lot discussions about a file size increasing when using base64: Is there any performance impact about choosing base64 vs multipart? We could think that exchanging data in multipart format should be more efficient. But this article shows how little do both representations differ in terms of size.

Check the base64.guru resourse for quick file encoding.

If your system works with big files or you need more granular approach then check this question on SO.

File Downloading

Preferable approach: Representational REST API

Content negotiation would be the best approach though. You should use a same endpoint for the metadata (JSON) and for the content (binary):

for the metadata (JSON)

Take a notice: we don’t return the full path to the document and we shouldn’t provide direct link to the document due to permission restrictions. Hence, we should use API endpoint for getting real file content.

GET /documents/{id}
Accept: application/json

Response:
{
  "data": {
    "id": "2612",
    "title": "PPA",
    "description": "Advanced description of the file",
    "created": "2016-06-15 10:06:14",
    "updated": "2016-06-20 12:15:39",
  }
}

for the content (binary)

GET /documents/{id}
Accept: application/octet-stream

Response: <binary data>

The Accept header should control what your endpoint returns. For example, if the header requests XML or JSON, that would return the file information, but if the header requests something like base64 or the generic binary type application/octet-stream, typically that would return the file itself.

That’s what "representational" means in REST. Nothing really returns the resource itself (in theory) – you return a representation of the resource based on what the client has requested.

To be honest, though it’s more right approach but it’s not really practical in many ways. I highly recommend to check "Practical approach".

Practical approach: Straightforward and simple

As we understand our document resource consists of both metadata and the actual content of the document. So we could support the following:

GET  /documents/{id}   <- Return a representation of the metadata of the document
GET  /documents/{id}/content   <- Return a representation of the content of the document

In this way everything is straightforward and we shouldn’t worry about different headers and so on.

Use Hateoas

This is not used at this moment but can be implemented in the future.

What is hateoas?

Hateoas stands for Hypermedia as the Engine of Application State

Example:

GET /api/1.0/users/1

{
   "username": "taleh123",
   "firstName": "Taleh",
   "lastName": "Ibrahimli",
   "_links":{ <- hateoas links
        "self":{
            "href":"http://localhost:8080/api/1.0/users/1"  <- resource self link
        },
        "orders":{
            "href":"hhttp://localhost:8080/api/1.0/users/1/orders" <- link to orders
        }
    }
}

Hateoas is very useful to build self-descriptive REST APIs.

Usefull links:

Leave a Reply

Your email address will not be published. Required fields are marked *