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 or https

  • 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:

  1. POST https://example.com/api/users

  2. POST https://example.com/api/students/yichen/courses

  3. PUT https://example.com/api/videos/v-id-1234567890/comments/c-id-1234567890

Don't do this:

  1. POST https://example.com/api/register-user

  2. POST https://example.com/api/add-in-list-of-courses-for-student/s-id-1234567890

  3. 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:

  1. Pagination

  2. Limits and Offsets

  3. Filtering

  4. Sorting

Guideline #1: Optional fields

Take these 2 endpoints:

  1. https://example.com/api/users/{user-id}

  2. 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