ReST API Design: A Beginner’s Guide
By: Janani Subbiah
Tags: Api Design, Programming, Development, Rest Api, Technology

Photo by Kaleidico on Unsplash

As a developer, chances are you have created, consumed (or both) APIs at some point in your career. If you are a mobile/web/UI developer, you have definitely interacted with APIs because the data to be displayed needs to come from somewhere and if you are a backend developer, you have definitely created APIs to interact with other services (UIs and other backend services alike).

One of the best things available to software engineers especially those that have to build APIs is the availability of an RFC for all things API related: status codes, headers, cookies, request/response bodies etc. With that said the not-so-good part about this is the fact that the RFC is just a spec i.e. teams can choose to follow them or not.

There is nothing that will stop a backend service from running if it sends back a successful response for an app crash. The spec is great when followed but nothing forces teams or frameworks to follow them.

But it is important to note that popular frameworks like Spring do force teams using them to follow some of these standards (e.g.: Cannot specify a request body for a GET request).

Getting back to the RFC, these specifications exist for a reason and creates a standard across all backend services. If I work on creating a backend service “A” that follows the RFC and then move to either creating another service “B” or making calls to third-party APIs, it would be SOOO helpful to have all these services follow a pre-defined spec. It ensures consistency because all services speak the same language.

In this post, I want to touch on how I like to get started with designing APIs. I don’t claim to be an expert by any stretch of the imagination and this post only aims to provide a starting place and not a deep dive. So, with these disclaimers out of the way, here is how I get started with designing APIs:

API Naming

API routes are the entry point to any backend service. Here are a few considerations as far as naming goes:

Identify the resource

If your application has a concept of user accounts and you are creating APIs that capture user login history (let us just assume the application needs this and users love this feature), one way to model the login history API would be to nest login history under users i.e. / users/{userId}/loginHistory. This is a clear and well-defined API that defines the hierarchy of login history. This is important because now you know login history is not its own stand-alone resource instead it is part of the user resource and always belongs to a user (identified by a userId).

Come up with a consistent API naming strategy

There are a few popular ways to name APIs. You can use a hyphen or camel casing (among other options, I am sure).

If we were to use hyphens loginHistory would look like this:

/users/{userId}/login-history

If we were to use camel casing loginHistory would look like this:

/users/{userId}/loginHistory

I am not sure if there is a hard and fast rule here except picking one and sticking to it. Consistency for the win!

Identify the operations

After we have identified a resource that needs to be managed, the next step is to identify what operations can be performed on those resources. Usually this is a subset of CRUD (Create, Read, Update, Delete).

Sometimes you only want to allow users to create something and not update it (could be you want to force users to delete a resource and re-create it without allowing them to update it).

After the operations have been identified we pick HTTP verbs. So for our example where we want to manage user loginHistory, we could decide we only want to allow the creation and fetching of user loginHistory and not allow updating and deleting.

HTTP Verbs

HTTP verbs tell the consumer of an API what operation the API provides. Most popular ones are:

GET: Read or fetch a resource

PUT: Update or modify a resource

DELETE: Delete a resource

POST: Create a new resource

It is important to use the right verb because that is like the unspoken language of what to expect in the response to an API (For example: A post more often than not returns the newly created resource in the response body with a 201 response status code, where 201 stands for CREATED).

The verbs also tells the user what they may need to provide in the request including if a request body is even allowed (if the RFC is followed). For example GET requests generally do not have a request body (because whatever identifier is needed to uniquely identify and get a resource should be a part of the URI/API route vs in the request body of a GET request).

While on the topic of HTTP verbs, I want to quickly talk about a couple of other not very commonly used HTTP verbs: HEAD and PATCH

HEAD

The HEAD verb is VERY similar to the GET verb except that the HEAD request cannot contain a response body. The HEAD request is used to test is a given URI is available. For example if we want to check if user with userId “123” contains loginHistory, we could make a HEAD request to /users/123/loginHistory and if that came back successful, we could follow up with a GET request to the same URI. This saves bandwidth while making requests to a certain URI especially if the response is huge.

PATCH

The PATCH verb is very similar to the PUT request and is also used to update a resource. The main difference is that a PUT request replaces the entire resource meaning all the fields of the resource whether or not they are being updated need to be passed to the API when using a PUT verb. But when we use a PATCH request we only need to pass instructions that will be used to update the resource including what needs to be done and which field update should be applied to.

In the case of our example, given we have decided to only allow users to create and get their loginHistory our APIs could look as follows:

POST /users/{userId}/loginHistory

GET /users/{userId}/loginHistory

Request Body

The next step is to figure out if there is going to be a request body and if yes, then come up with a schema for what that should look like. According to the RFC, GET requests are not to contain request bodies so we are going to follow that and only create a request body for the post request. So a sample example loginHistory POST request body, in JSON could be:

{

“time”: “1605202745”,

“appVersion”: “5.2.0”

}

The userId has been left out of the request body because it should be fetched from the API route. Also other additional fields required to capture all loginHistory related information can be added to the body above.

Response Status

The response status of a response should convey two things:

Did the operation succeed?

Next steps (if any)?

There are many many status codes available (including RFCs for when and what to use). Because the first thing the response status conveys is whether the operation succeeded or failed I think it goes without saying that using a 2xx (the 200 series status code that indicates a success) when an error occurs is a big no NO.

When it comes to status codes it is important to pass the right sub-status code in the response. For example a 200 means OK meaning everything went well while a 201 is used to indicate that a resource was created successfully. So a 200 would be apt for a GET/PUT request while a 201 would be more appropriate for a POST request (assuming the POST request creates an object). Using a 200 for a POST request would not be a problem mainly because it would convey a success as well. But, a 201 will be better suited because in addition to indicating that the request was received and processed successfully it also indicates that a new resource was created. Along those lines for successful DELETEs it is good to use the 204 (no content) status code indicating to the calling client that while the request was successful, there is no response body that is to be expected (i.e. if there is not going to be a response body).

Success codes when used interchangebly have less far reaching consequences than when failure codes are used interchangeably. For example using a 401 (unauthorized) in place of a 403 (forbidden) would be bad because it gives the wrong indication to the user (or the calling client which could be a UI component). A 401 indicates that the authentication credentials presented do not match the records that the backend compares against. A 403 indicates that the user is trying to access a resource that they do not have access to. A solution to fix a 401 is to change the authentication credentials presented to the application while a solution to a 403 could be as simple as “sorry, nothing to fix here”. More often than not, the right error status code will dictate next steps. So it becomes all the more important to use the right status code especially when there is an error.

In general it is very important to follow the RFC closely while picking status codes because they mean something (and apps/services using your APIs will expect them to be consistent with the RFC because that is the common reference for everyone!) and sometimes it can be easy to get them confused.

Response Body

The next most important step in API design, is settling on a response body. It is very common practice to make sure the response body contains everything that the calling client needs (e.g.: If a web app is making a request to an API to fetch the login history of a user, from the backend we make sure to provide them with all the information the web will need for display purposes). On rare occasions, we might need to more than required information to the calling client, especially if the backend does not leverage any caching, the additional information does not originate from the backend and the backend will need this information to process an incoming request. An example will help right? Absolutely. Let us consider a scenario where a backend makes a call to service provider X to fetch some information and pass that back to the UI as a part of the response though (let us assume) the UI does not use that information at all BUT because the backend does not cache and service X takes a lot of time to response to requests (lets continue with the assumptions), then it will be helpful to have that information passed to the UI and passed back so that the backend can use that information to do whatever it needs to.

Another important consideration I like to make when finalizing APIs is to make sure responses are not bigger than they need to be. A good question to ask if you have very big response bodies is to ask if it might be possible to break up the single API into multiple APIs. For example if you have a messaging app, instead of having a single API that serves up all the information relating to say messages displayed on a page (say we have pagination implemented and we are displaying 20 messages per page). So one option would be to have 20 messages that contain all the message information or 20 messages containing only that information that will need to be displayed on the list of messages page and another API to get more information to be displayed on the page when a user clicks into a message. Deciding between the two may need to consider other factors like bandwidth considerations, processing time, response time etc. But if these considerations can be controlled, it might be a good idea to consider that latter: Making two APIs one for a list of messages with minimal information about the messages(/messages?page=2) and another more specific API to get additional information about a single message (/messages/{messageId}).

When it comes to error scenarios, response bodies must be constructed in such a way that information about what has happened and indications as to what to do to fix errors will be helpful.

Query Parameters vs Path Variables

API developers are free to decide if a certain field needs to be in the query parameter or path variable. Depending on the implementation we can get both of these variations to work just fine. Two rules of thumb that have helped me decide where a certain field goes is:

To think of if the request variable is used to uniquely identify a resource or if the request variable is more like a search criteria.

If the variable used is available to all resources (if we consider login history and say only certain login history resources have a certain field then using that field as a path variable is not advisable because a URI with that variable cannot uniquely identify a login history resource). For example we need a userId to uniquely identify a user but say search parameters like isAccountActive is a search criteria and there is not a way to get a particular user using just this field. But it is important to call out an additional rule I add on top of this. If there are multiple fields that can be used to identify a resource, I prefer to use the one that the end user of the system has no control over. For example if a user can be uniquely identified using the userId and the userName, I would pick the userId because the userName can be changed by the user but not the userId (and that is the way it would need to be the user should not have an option to change the userId).

Conclusion

API designing is a lot of fun because there are specifications and rules around what is universally understood and expected while also allowing some room for creativity. I think the line is thin but following the spec or at least giving it an honest attempt goes a long way in ensuring consistency not just for the application that contains the APIs but also for those that consume it.