If you’re a developer working on an app (i.e., everyone) you’re probably either using or building some basic CRUD endpoints: think creating a new user, updating profile information, etc. If this is your first time doing so, there are some non-intuitive best practices to follow around URL naming, which HTTP methods to use, and how to handle versioning. So we’ve put together this collection of tips that will (hopefully) help make your first few endpoints easy to use, performant, and secure.
You’ve probably used endpoints ranging from /users to /users/edit to /v1/users/edit/update. But it turns out there are some standards worth following.
Endpoints should bear the name of a noun and ideally not contain verbs. Think of your business concept (i.e., a user) and build one endpoint for them, with separate HTTP methods taking place of the things you’d want to do with that user data.
If you were to use a verb-based approach, the /createuser
endpoint would be particular to users but can only create a user. Other actions with this approach will include /edituser
for editing user details and /deleteuser
to delete a user. This means you have multiple endpoints for a single entity when you could have just one. Instead of using a verb, you can provide a /users
endpoint and support different actions with HTTP action verbs.
For instance, for the /users
endpoint, you can use GET /users
to fetch users and POST /users
to create a user. You have more simplicity here because you get to provide only one endpoint with different HTTP actions. This of course only works with basic CRUD operations that map 1:1 with HTTP methods; for anything more complex you will need to couch that logic in the URL or your headers / body.
While these two implementations serve the same purpose, the use of a noun (POST) with HTTP action verbs is better because you don't have to manage different URLs for a single resource.
Additionally, nouns should be in their plural form unless they represent a single resource. Unnecessarily long nouns should also be avoided because they make things look messy and unorganized. For example, you should use /subscribers
instead of /api-subscribers
.
Case sensitivity in URLs causes ambiguity: it’s best to stick with lowercase (i.e., /user
instead of /User
) when declaring your API endpoints.
Adding resource identifiers (i.e., resource IDs) in a URL can get tricky and cause security risks like Insecure Direct Object Reference (IDOR). In an IDOR, an attack is carried out by manipulating the raw resource ID of a database entity revealed in the URL.
This security issue can be eliminated by not including the actual resource ID from the server in the path. Instead, you should use a Universally Unique Identifier (UUID) generated from the server, represented as an alphanumeric.
When using resource IDs in URLs, you should use them as a path parameter like /orders/{orderId}
instead of using them as a query parameter like /orders?id={orderId}
. As a general guideline for REST APIs, path parameters should be used for resource identification while query parameters should be used for resource sorting and filtering.
In API design, good URL implementation focuses on making sure the URL paths are as simple as possible even if resources are nested. You should avoid more than two levels of nesting in the URL paths.
In two-level nesting more than two nested resources cannot be used. For example, posts/:postId/comments/:commentId
is nested in two levels and is more readable compared to customers/:customerId/projects/:projectId/orders/:orderId/lines/:lineId
which is more than two levels.
API versioning is a controversial part of API design because there are different ways to version APIs and different teams have different ways of doing things. In the end, it's all about the users of your API. How do you version your API to ensure that your users know how to use it correctly? Below are two ways to version your API and its limitations:
URI path versioning means specifying the API version in the path. This might seem harmless, but it can become complicated for users. For example, api/v1/products
can either indicate version one for the products resource or version one for all the API endpoints. Most of the time, the versions are prefixed for all endpoints and the user may be uncertain how to proceed.
This approach can work if your API doesn't change frequently, but if it does, you should avoid it so that you avoid frequent breaks because of changes on the user’s end.
API versioning with headers requires passing the version in the request object. This method is more crude compared to URI path versioning. However, it can still pose the same problem as that of the URI path: the user may not be sure if the version is for an endpoint or the entire API.
The two approaches mentioned above have one thing in common: the user is unsure of the versioned resources. To avoid this ambiguity, it’s important to make a disclaimer in your documentation or any format you find appropriate to make sure the users of your API services are well-informed.
HTTP verbs are relatively straightforward when it comes to API versioning. Always make sure to use the appropriate HTTP verb for the operation specified.
For example, the GET
method should only be used to read or fetch resources from the server. It should not be used to create or update a resource on the server. GET
is considered the operational method for the R (read) in CRUD.
The POST
method is used to implement the C (create) operation in CRUD. It should be used to create a new resource.
The PUT
method updates a resource on a server. A PUT
request to the /users/20
can be used to update the profile of the user with an ID 20.
A PATCH
request works like a PUT
request but with only one difference. The PATCH
request is used to update a resource with partial data, like updating a user's email address, whereas the PUT
method is used to update the entire resource.
And finally, the DELETE method removes resources from the server.
When using HTTP verbs, always make sure to use the appropriate method for their respective operations to avoid ambiguity in implementation. You can use POST
to create a new resource and not have to update an existing one. Likewise, you can use PUT
to update the entire resource and not a field in the resource.
Reader