REST API Design

This short guide provides practical guidelines to design REST APIs. For each guideline, I have provided why it should be done that way. These guidelines have served me well over time. If you are a purist, you may not agree with all design guidelines and that is okay.

Terminology

  • Resource - An object stored in the server.
  • Endpoint - URL that is used to manipulate the resources. For e.g. https://api.example.com/v1/users.
  • Method - HTTP method used while invoking the endpoint. For e.g. GET, POST, PUT, DELETE, PATCH.
  • Payload - Body of the request or response. Usually this is a JSON object. Some times it could be of other types like xml or text.
  • Query parameters - Parameters that are sent as part of the URL. For e.g. ?skip=100&limit=20.
  • Schema - Structure and type of fields in payloads and query parameters.

Version your endpoints

There can be two kinds of non-backward compatible changes: structure of payload and behavior. In both cases, having a version number as part of the URL will help you. Anytime when you need to make a non-backward compatible change, bump up the version number. This will ensure that the existing clients are not broken.

# Bad
https://api.example.com/users/123

# Good
https://api.example.com/v1/users/123

Consider the case when you were always returning rows sorted in ascending order. But you decided to change the sorting to be descending. Though the payload schema does not change, this behavior change will most likely break your clients.

Endpoints should contain resource types

Endpoints should contain resource types. They should not have operations. Resource types should appear in plural form. When there is a single resource to be retrieved, its identifier (typically primary key) should be specified in the endpoint.

# Bad
https://api.example.com/v1/create_user
https://api.example.com/v1/delete_user

# Good
https://api.example.com/v1/users # All users
https://api.example.com/v1/users/123 # Single user with id 123
https://api.example.com/v1/users/123/groups # All groups user 123 belongs to

Usually when you have more than one ID in your end point, it is a design smell.

Design of payload

A simple respresentation of the user resource is given below.

{
    "first_name": "John",
    "last_name": "Doe"
}

In case any additional fields that are not part of the resource need to be added, I use a special field name called meta.

{
    "first_name": "John",
    "last_name": "Doe"
    "meta": {
        "status": 200,
        "message": "OK"
    }
}

We may need to send a collection of resources as part of request or response. For instance, the caller may want to retrieve first 10 users. It is a good idea to design both request and response payloads to be objetcts, instead of arrays. We can send any additional information as shown below.

{
    "users": [
        {
            "first_name": "John",
            "last_name": "Doe"
        }, {
            "first_name": "Jack",
            "last_name": "Smith"
        }
    ],
    "meta": {
        "status": 200,
        "message": "OK",
        "total_records": 2,
        "start_index": 0,
        "records_per_page": 10
    }
}

This design is flexible enough to address many of the commonly occuring use cases.

Naming of fields

You can name the fields in any convention: camelCase or PascalCase or snake_case. But pick one an follow it consistently. My personal favorite is snake_case. It is easier to read.

Don't use kebab-case. Kebab case strings are not valid identfiers (variable names) in most languages like JavaScript and Python. If your backend server is written in JavaScript, it will be easier on the eye to read obj.first_name, instead of obj["first-name"].

When it comes to naming resources in the URLs, follow the same guideline.

HTTP method indicates the operation

Use the HTTP method to indicate what operation needs to be performed. If we were to map the CRUD operations to HTTP methods:

  • POST - Create
  • GET - Read
  • PUT and PATCH - Update (difference explained later)
  • DELETE - Delete

When creating a resource, usually the client does not specify the id for the resource. Server generates the identifier.

The example below creates a new user.

POST https://api.example.com/v1/users 
{
    "first_name": "John", 
    "last_name": "Doe"
}

# Response
200 OK
{
    "id": 123, 
    "first_name": "John", 
    "last_name": "Doe",
    "meta": {
        "status": 200
    }
}

# Special case: create multiple users
POST https://api.example.com/v1/users
{
    "users": [
        {
            "first_name": "John", 
            "last_name": "Doe"
        }, {
            "first_name": "Jack", 
            "last_name": "Smith"
        }
    ]
}

# Response
200 OK
{
    "users": [
        {
            "id": 123,
            "first_name": "John", 
            "last_name": "Doe",
            "meta": {
                "status": 200
            }
        }, {
            "id": 456,
            "first_name": "Jack", 
            "last_name": "Smith"
            "meta": {
                "status": 400,
                "message": "User already exists"
            }
        }
    ]
}

Difference between PUT and PATCH

There is a difference between PUT and PATCH. PUT requests will contain all fields of the resource in the request body. Once PUT succeeds, the resource has all fields replaced with what came in the request body. So any missing fields in the request will result in those fields being removed from the resource. In case of PATCH, only the fields that are present in the request payload are updated. Any missing fields in the request payload are left as it is. Let us understand this using an example.

PATCH https://api.example.com/v1/users/123
{
    "id": 123, 
    "last_name": "Smith"
}

# Response
200 OK
{
    "id": 123, 
    "first_name": "John", 
    "last_name": "Smith",
    "meta": {
        "status": 200
    }
}

PUT https://api.example.com/v1/users/123
{
    "id": 123, 
    "last_name": "Smith"
}

# Response (notice that there is no "first_name" field)
200 OK
{
    "id": 123, 
    "last_name": "Smith"
    "meta": {
        "status": 200
    }
} 

Nesting of resources

When you are retrieving a resource, you may want to retrieve another resource that is referenced by the top level resource. Often these two resources will have "has many" or "belongs to" relationship. While querying for the resource, I use a query parameter called include to indicate that the other resource should be included as well.

# While querying for user, retrieve the groups as well.
GET https://api.example.com/v1/users/123?include=groups

# Response
200 OK
{
    "id": 123,
    "first_name": "John",
    "last_name": "Doe",
    "groups": [
        {
            "id": 456,
            "name": "admin"
        }, {
            "id": 789,
            "name": "billing"
        }
    ]

Deleting relationship between resources

Let us take the example from the previous section where a user belongs to two groups. Let us say we would like to remove the user from one of the groups ("billing"). We invoke DELETE method on the full resource URL.

# Deletes only the relationship, NOT the group "billing".
DELETE https://api.example.com/v1/users/123/groups/789

# Response
200 OK
{
    "meta": {
        "status": 200,
        "message": "Successfully removed user 123 from group 789"
    }
}

Filtering operations

You may need to retrieve resources based on some filter criteria. I prefer to make use of JSONpath like filters. You may restrict the operations to a few: equals, not equals, less than, etc.

# Retrieve all users whose first name "John"
GET https://api.example.com/v1/users?first_name=John

# Retrieve all users whose first name is NOT "John"
GET https://api.example.com/v1/users?first_name=!John

# Retrieve all users who belong to group "admin"
GET https://api.example.com/v1/users?groups[*].name=admin

# Retrieve all users who belong to group "admin" and return their group info as well.
GET https://api.example.com/v1/users?groups[*].name=admin&include=groups

Conclusion

Most of my suggestions are heavily influenced by Rails and Django. These guidelines may not be exhaustive. I hope they act as a good starting point. If you have any feedback, please share with me through my LinkedIn or Twitter.

links

social