By Gourav Dhar
This article was originally published on June 27, 2021, on Gourav Dhar’s personal blog and has been reshared here with permission from the author. Gourav currently works at Pulse Labs as a backend developer.
Have you created a service and want others to use it? Well!!! APIs are the way to do it!
What is an API?
Suppose you have written a program that performs a task (maybe it takes an input and returns an output based on it). You would want a way for other people to use your program — you need APIs for this purpose.
API stands for Application Programming Interface, which is a software interface between external users or other services and your program. We can expose API endpoints on the internet through which other applications can interact with our program.
Designing good APIs
There’s no one right set of characteristics. Whatever design serves the purpose in an efficient manner is a “good API design.”
As a software developer, I would want the APIs to have the following set of characteristics to ensure consistency, security, and efficiency in my system.
SRP (Single Responsibility Principle)
Ensure each API has a single responsibility to reduce ambiguity. This makes certain that the operation performs atomically and that our database is consistent with what the clients expect. This way, the client will know (and understand) that if the API fails or returns an exception, then no part of the operation was performed.
Let’s take an example — Suppose we want to create a user and assign a label to it. If we use a single API to execute the operation and the API fails and returns an exception, there might be confusion if the user was created and an exception occurred while creating the label, or if exception was returned while creating the user and no user was created. So, it is better to create two separate APIs: one for creating users and one to update parameters/assign labels to users.
Naming the API
Name the APIs so that it becomes easy for the clients to guess the purpose of the API. The type of request (GET, PUT, POST, DELETE) adds context to the naming as well. This way, if your server has a bunch of APIs exposed, it becomes easy for the client to manage the APIs while using them. It also makes debugging easier.
For example, if you want to get details of a user, you could name the API as
GET - /api/user/{userId}
Similarly, to delete the user, you could name it as
DELETE — /api/user/{userId}
Abstraction
Return only what is needed.
- Exposing only what is needed keeps presentation neat and tidy, creates less confusion, and, in some cases, reduces coupling.
- Returning less data reduces response time and saves internet bandwidth. (And makes surfing easy for everyone!)
- You don’t want to expose certain information which can be exploited by hackers to get into your system. Be careful!
- Sometimes you may be convinced to return extra information through your API by apprehending certain requirements for the future. Trust me, this is not a good idea! In most scenarios, these requirements either won’t come up or, if they do come up, you will end up having to create a separate API for them anyway.
Logging
Logging API name, request parameters, response time is a good practice that helps to debug issues and maintain your program.
It is also important that you don’t accidentally log user sensitive information like email, passwords, credit card details, etc. which may be received as part of the API request, leading to a security vulnerability.
Validate User Roles / Permissions
Add user role validations to APIs. It seems obvious, right? You don’t want the regular users to have access to APIs meant for admins!
Throw Custom Exceptions
In case your API fails (whether due to some internal function failure, a database network call, an invalid input, etc.), it is a good practice to throw a custom exception message along with the exception. (One way to do this is creating a custom exception class).
This helps a lot while reading logs and debugging issues. It also gives the client an idea of what is going wrong.
The way to do this would be to identify probable scenarios where the API is likely to throw an exception, like when a client is trying to access information he/she is not entitled to or if a client requests to modify user details which don’t exist.
Response Time
Perhaps a little obvious, but, the less response time the better!
- Make sure to place the endpoint on the correct micro-service to optimize the number of external calls made to other microservices. When making database calls, try to reduce the number of calls (by maybe fetching all the data together or other ways).
- Another way would be to receive extra parameters as part of the request body which would prevent the need to make extra database calls to fetch that information.
- Try to use Threads and Futures wherever possible. Multi-threading generally makes execution faster!
- You may also want to use caches at the API level. The data returned by APIs (which are called frequently and rarely updated) can be stored in the local cache of the server.
Handling Scale
Suppose all of a sudden you see a large influx of clients using your API. (After all, things tend to increase exponentially!) Your database and server have a certain limit on the number of connections and your system was not designed to handle such a scale! Obviously, you will eventually have to scale your system up, but, in the meantime, as a hotfix, you could temporarily remove certain unimportant database calls or functions so that your API can still perform. This is a good thing to keep in mind while designing APIs.
Pagination
Pagination is a must to reduce complexity and the load on resources. Suppose you are exposing an API that returns registered user details. Your system could have 1,00,000 registered users. One way to go around this would be to fetch all the users from the database, process all their information, and send the results back over the internet. Well, there are a few major problems with this approach:
- Sending so much data over the network consumes a lot of bandwidth.
- Querying the database with so much data, sending, and processing increases the response time. (You don’t want your client to have to wait more than a few seconds for the response.)
- There is a high chance the client won’t even use all the information which would return with the request, which is a waste of resources.
Instead of fetching information from all the users in the database, use filters in the request body and pagination:
- Now, the API is querying certain fragments of data (maybe tops 100 records) from the database and returning those records along with a token which the client can again use to fetch the next fragment (maybe the next 100 records). This way, the response time is less, we are using only the information which is useful, and it reduces complexity and load on resources.
Rate Limiting
Set rate limits — You may want to limit the number of API calls made by a user (or a token) within a given time frame. If a user exceeds that limit you throttle their request for that time period. There can be several reasons why you want to add rate limits:
- Prevents resource starvation.
- Keeps a check that you are not denying service to other clients because one client is using your API a lot.
- Prevents malicious attacks like DDOS (Distributed Denial of Service) by hackers
You can either rate limit programmatically or you can charge clients a certain amount of money whenever they make an API call.
While I think the above rules contribute to good API designs, you may be in situations where you would need to compromise on certain design rules to make some other aspect of the system more efficient. As a software developer, it’s your responsibility to weigh the pros and cons and make a call. After all, designing systems are all about trade-offs (time vs space).
To get updates on more interesting topics like this, follow me on Medium - https://blogs.gourav-dhar.com. You can also reach out to me at gouravdhar.iitr@gmail.com to discuss doubts or suggestions. Feel free to comment and share your experiences and thoughts.