URI Design
When it comes to designing URIs, it's important keep everything clean and consistent.
Before we take a look at the guidelines, we need to first look at the actual structure of the URI.
According to RFC 3986, the generic URI syntax is as follows:
URI = Protocol "://" Host ":" Port "/" Path [ "?" Query ] [ "#" Fragment ]
Example: https://www.domain.com:8080/user/profiles?lastname=Smith&firstname=John#topheader
Protocol; e.g.
http
orhttps
Host; e.g.
www.domain.com
Port; e.g. 80 (this will be hidden), 8080, 3000, etc..
Path (consists of fragments); e.g.
/user/profiles
Query Parameters (Optional); e.g.
?lastname=smith&firstname=john
Fragments (Optional); e.g.
#topheader
URI Formatting and Styling
Very similar to following a set of coding conventions, URIs should follow a set of formatting guidelines. Here are 5 guidelines that I have personally followed in the past couple of years when formatting the URIs of my API endpoints.
Guideline #1: URIs should contain only lower case characters
This is because URIs are case-sensitive (except for the protocol and host) according to RFC 3986. Therefore, no camelCase or other use of capitals.
Guideline #2: URIs should not contain a trailing slash ("/")
This is because every character within a URI counts toward a resource’s unique identity, the following two URIs are not equal.
http://website.com/api/cars/1
!== http://website.com/api/cars/1/
Guideline #3: Use the hyphen ("-") for word separation
Many times, people will use underscores ("_") for word separation, but this has drawbacks since the default behavior of many tools (browsers for example) will indicate an underscore ("_") when there are clickable hyperlinks. Using only hyphens ("-") to make the visual formatting a lot more clear and evident.
Guideline #4: Do not include file extensions in URIs
Do not use any file extensions like .json
or .xml
in your URI. Instead, make use of media types though the Content-Type
header to determine how to process the body’s content.
Guideline #5: Avoid white spaces in URIs
Do not use blanks spaces as this will confuse whichever client you are using when calling your service endpoint. Whatever client you are using, may it be cURL
, node-fetch
, axios
, or jQuery Ajax
, not all clients will support the encoding and decoding of the blank white spaces. It is best to just stick to hyphens ("-").
URI Path Design
When it comes to designing resources and their respective pathings, I generally like to focus on 3 aspects:
The first is to always think of your endpoints as resources, so anything you return should be a resource representation. There are mainly 2 types of resources, a singular resource and a collection of resources.
The second is to make use of hierarchies properly and to nest them accordingly.
The third is to avoid treating your URIs as actions or functions and to only create functional endpoints as a last resort, because as mentioned, you should always think of your URIs or endpoints as resources.
Here are some guidelines that I have personally stuck to.
Guideline #1: Resource paths should be plural
Resources should be nouns and pluralized. This is very important as the other guidelines build on top of this concept.
Here are some good examples:
https://example.com/api/students
https://example.com/api/courses
https://example.com/api/teams
https://example.com/api/people
https://example.com/api/comments
The reason why we want to design URIs as resources is because the HTTP method verbs already describe a corresponding CRUD-based style operation.
Guideline #2: Use identity-based values for fetching singular resources
Building on top of the previous guideline, giving a URI the ability to dynamically fetch any single resourced based off of an ID value is quite common.
Here are some good examples:
https://example.com/api/students/john
Here, "john" is the identifier.
https://example.com/api/teams/raptors
Here, "raptors" is the identifier.
https://example.com/api/users/id-123456789
Here, "id-123456789" is the identifier.
Guideline #3: Separate resources via hierarchies
This means, separate the many different relations of nouns with the use of '/' in order to create some sort of hierarchical structure. This creates an inheritance pattern of one-to-one, one-to-many, and many-to-many relationships between singular resources and collections of resources.
Here is one good example:
https://example.com/api/schools/harvard/students/yichen/courses
As you can see we are using the ID based approach for fetching singular resources while going down the chain. In this case, schools, students, and courses, are the different levels of hierarchy. The IDs are harvard, and yichen. This is quite easy to understand and can give us many flexibilities. For example, if we wanted to only fetch all the students, then we would intuitively believe this is the URI to call.
https://example.com/api/schools/harvard/students
Here are some other good examples:
https://example.com/api/teams/raptors/players/derozan
https://example.com/api/videos/v-id-1234567890/comments
https://example.com/api/companies/facebook/departments/accounting/people/john
Guideline #4: Avoid actions and verbs in the URI
Do not use actions and/or verbs to describe the URIs, that's what the HTTP method verbs are for. Only create functional endpoints, meaning URIs with actions/verbs if you have to, try to avoid it as much as possible.
Do this:
POST
https://example.com/api/users
POST
https://example.com/api/students/yichen/courses
PUT
https://example.com/api/videos/v-id-1234567890/comments/c-id-1234567890
Don't do this:
POST
https://example.com/api/register-user
POST
https://example.com/api/add-in-list-of-courses-for-student/s-id-1234567890
PUT
https://example.com/api/update-comment-by-id/c-id-1234567890
As you can see from example 1, the HTTP verb POST already implies a user will be created, adding any sort of verbs like "create" or "register" is redundant.
Example 2 is a good depiction as well as the POST
indicates that we are creating a collection of courses for the student with the ID of "yichen". There is no need to put any verbs like "add-courses" or "assign-courses" in the URI.
Example 3 uses the HTTP method verb PUT
, same as POST
, the example above is quite clear, "update the comment with that particular ID".
A rule of thumb is to use an action verb when a URI does not adhere to a CRUD operation. Some examples of this include an API endpoint used to log a user in or sending a one time email.
URI Query Design
Now comes the optional part of the URI component, the query strings. A very common question is when should one use a query string versus a URI parameter?
The rule of thumb that I always tell myself is that query string parameters should be used the most with optional fields, this means any fields that are not required.
The other rule of thumb is that there are really only 4 patterns/scenarios of using query strings, and they are:
Pagination
Limits and Offsets
Filtering
Sorting
Guideline #1: Optional fields
Take these 2 endpoints:
https://example.com/api/users/{user-id}
https://example.com/api/users?user-id={user-id}
What is the difference between them?
The difference between 1 and 2 is that the URI for the first endpoint is https://example.com/api/users/{user-id}
whereas the URI for the second endpoint is https://example.com/api/users
. Do you see the difference?
For 1, https://example.com/api/users/{user-id}
means that "user-id" is a required dynamic field that is part of this URI. Just because this particular endpoint exists does not mean https://example.com/api/users
exists as an endpoint. This means although you can get a user with a specified "user-id", there is no endpoint for getting all the users.
For 2, the endpoint is https://example.com/api/users
and the "user-id" query string parameter is optional, so the original endpoint will get a list of users, and if the optional parameter being user-id is present and specified, then it will get a specified user.
Guideline #2: Pagination, limits, filtering, and sorting should all be optional
If you read the preface of this book, you will remember I mentioned something about writing over 150+ endpoints at digital agency job. Want to know why I ended up writing over 100 endpoints? It's because I didn't know how to handle situations that involved paginating and filtering.
Take this endpoint for example:
https://example.com/api/students?page=2&limit=30&gpa=>3.0&sort=desc
This says I want to get a list of students who's gpas are above a 3.0 sorted from highest to lowest. I only want 30 students per page and I want the 2nd page.
What if I didn't set a page number or limit? Then it would look something like this:
https://example.com/api/students?gpa=>3.0&sort=desc
What about if I just wanted all the students? Then it would look something like this:
https://example.com/api/students
Great, no problems here.
Now let's see how this would play out if I converted those 4 fields into required (or non-optional) fields as part of the URI.
Our URI definition would look something like this.
https://example.com/api/students/page/{page-number}/limit/{limit-amount-per-page}/gpa/{>|=|<(number)}/sort/{asc|desc}
If we wanted to the same results as above, then we would call the endpoint as follows.
https://example.com/api/students/page/2/limit/30/gpa/>3.0/sort/desc
What happens now when I don't want to set a page number or a limit?
I can't do this, because then this would be a new URI endpoint.
https://example.com/api/students/gpa/>3.0/sort/desc
What about if I just wanted all the students, so I may do something like this.
https://example.com/api/students
Oh, but wait, we technically never created an endpoint for that.
This is what we call a "telescoping characteristic", where because the fields are not optional and are fixed in a certain order, we are unable to dynamically insert values with any level of flexibility. Hence, the most commonly used fields when designing URIs such as pagination, limits, filtering, and sorting should all be optional.
Last updated