.NET Nakama

Improving your .NET skills

REST Web API in Practice: Naming Endpoints, Filtering, Sorting, and Pagination

October 04, 2021 (~18 Minute Read)
API DESIGN REST REST WEB API PRACTICAL SUGGESTIONS

Introduction

The REST architectural style emphasizes a uniform interface between components, in which the information is transferred in a standardized form (.NET Nakama, 2021 September 4). One of the architectural constraints (identification of resources) to achieve that is to uniquely identify the location of each resource through a single URL.

In practice, when we are trying to design the URLs structure, various questions and possible conflicts between the team members may arise. In this article, we will see some practical suggestions for adopting consistent: naming conventions in our URLs (API endpoints) and URL representation of the filtering, sorting, and pagination operations.

Naming the Endpoints

Let’s remember some basic concepts regarding the REST resources and collections (.NET Nakama, 2021 September 4):

  • A resource is any information that can be named, an entity (e.g., person, ticket, product, etc.), document, file, etc.
  • A group of resources is called a collection (e.g., persons, tickets, products, etc.).

Our priority when naming our endpoints is to identify the resources that our API will expose to the consumers. Therefore, we have to be very careful to avoid creating APIs that mirror the internal structure of our database(s) (Price E. et.al, 2018).

A resource can represent multiple internal items. For example, we can expose a product resource to the consumer, which might be implemented internally as several database tables (e.g., service products, physical products, description translations, metadata etc.).

The identification of the resources should be made based on the consumer needs and business logic.

Nouns or Verbs

A URL in REST should refer to a resource (represented as a noun) and not to an action (verb) (Au-Yeung J.,& Donovan R., 2020) (Price E. et.al, 2018). The actions in REST will be defined as HTTP verbs (GET, POST, etc.), not in the URL.

URL examples using Nouns URL examples using Verbs
https://api.mydomain.tld/products https://api.mydomain.tld/create-product
https://api.mydomain.tld/orders https://api.mydomain.tld/get-orders

Singular or Plural

It’s decided! We will use nouns to refer to our resources, but now we should decide if we will represent a resource in the singular form (e.g., product) or plural (e.g., products). Let’s see some examples to have a better understanding of the dilemma.

Resources URL Description
Plural form https://api.mydomain.tld/products/123 The /products/123 path refers to a specific product collection resource (with id=123).
– //– https://api.mydomain.tld/products The /products path refers to the collection of all products.
Singular form https://api.mydomain.tld/product/123 The /product/123 path makes sense as we refer to a specific product.
– //– https://api.mydomain.tld/product However, the /product path is unclear if we refer to one or many products.
Singular and Plural form https://api.mydomain.tld/product/123 By combining singular and plural forms, we do not have consistent URLs and may increase our code complexity.
– //– https://api.mydomain.tld/products – //–

It is generally recommended to use plural nouns to reference a collection of resources in the URLs (Price E. et.al, 2018) (Au-Yeung J.,& Donovan R., 2020). In this way, we have a clear and consistent representation of the resources in the URLs.

Resource Relationships (Nesting, Hierarchy, Sub-collections)

The different types of resources can have relationships among them to show which resource type contains the other. Several terms are used in the literature to refer to the relationships of resources, such as Nesting, Hierarchy, and Sub-collections.

To represent the relationships among resources in a URL, we use the slash “/” character to create a “path.” For example, the GET /users/5/orders endpoint represents the logical one-to-many relationship of the users with orders, meaning that it will return all orders (many) of the user with id=5 (one user).

There is no limit in the depth of the path of the relationship. However, I recommend using a path that is not more complex than: /collection/item/collection/item.

Flat Endpoints

Some developers do not accept using resource relationships in the URL path because it can be confusing and complex (Florimond Manca, 2018). Using flat endpoints is not bad when our resources have simple relationships, and it may be preferable depending on the consumer’s needs.

The following examples show a simple resource relationship of users and orders, and their representation with a flat endpoint compared to a resource relationship endpoint.

Resource URL Example Description
https://api.mydomain.tld/users/5/orders Get the orders of the user with id=5.
https://api.mydomain.tld/orders?user=5 A flat endpoint to get the user’s orders (with id=5) by using a query string filter.
We will see more details about filtering in the following section.

Derived Resources

Let’s assume that we are implementing some endpoints to GET the available products and purchased products of a specific order, as follows:

  1. The available products (e.g. https://api.mydomain.tld/products).
[
    {
        "productId": 2,
        "Title": ".NET T-Shirt",
        "Price": 10,
        "Sizes": ["M","L"]
    },
    {
        "productId": 3,
        "Title": "C# T-Shirt",
        "Price": 8,
        "Sizes": ["S","M","L","XL"]
    }
]

2 . The purchased products of a specific order:

[
    {
        "id": 1,
        "productId": 2,
        "Title": ".NET T-Shirt",
        "Price": 10,
        "Size": "M",
        "Quantity": 2,
        "Discount": 5,
        "TotalPrice": 15
    }
]

Conceptually, we will return a list of products in both cases, but their data would be different. In the first case, we will get the details of each product (base resource). However, in the second case, we will get additional information about the purchase of each product item (derived resource), e.g., the quantity, discount, total price, etc.

The following table shows how we could represent the URL of the cases above with flat endpoints compared to relationships endpoints. For this scenario, I prefer the URL endpoints that clearly illustrate the relationships of the resources and not the flat endpoints.

Resource URL with Relationships Flat Resource URL Description
https://api.mydomain.tld/orders/123/products https://api.mydomain.tld/order-products?order=123 The purchased products of the order with id = 123.
https://api.mydomain.tld/orders/123/products/1 https://api.mydomain.tld/order-products?order=123&id=1 The first purchased product (id=1) of the order with id = 123.

Non-Resource Endpoints for Processes and Executable Functions

In REST, we should represent our resources as plural nouns (e.g., products, users, etc.). However, all rules have their exceptions. There are non-resource scenarios in which we cannot represent the API operations with nouns (Price E. et.al, 2018) (RestfulApi.net, 2021). For example, there are processes and executable functions such as the following which a verb is preferable.

  • /cart/checkout
  • /server/321/restart
  • /search
  • /player/stop

In the non-resource scenarios where the actions don’t have parameters, we could use action results as resource property (see them as resources). For example, when we restart a server (e.g., using the endpoint /server/321/restart), the server’s status is modified (e.g., from Running to Restarting, Stopped, etc.). In such a case, we could avoid using a verb, and instead, use the PATCH method to update the specific resource property (e.g., the status to Restarting).

Method: PATCH
URL: /server/321

{
    "status": "Restarting"
}

Trailing Forward Slashes

A common question when naming endpoints it’s if we should use a trailing forward-slash (“/”) or not. Conventionally, using a trailing slash indicates a directory (hierarchy), and not using a trailing slash denotes a file (GNU, 2021).

URL Example Description
https://api.mydomain.tld/products/ A URL with a trailing slash indicating a directory.
https://api.mydomain.tld/products A URL without a trailing slash denoting a file.

Google states that they treat each URL separately (and equally) regardless of whether it’s a file or a directory, or it contains a trailing slash, or it doesn’t contain a trailing slash (Maile Ohye, 2010).

The important thing is to choose one way or the other and be consistent. That means that we should redirect (301 - Moved Permanently) the non-preferable way to the other. We will not serve duplicate content in the same URLs (i.e., the URL with and without a trailing slash) by performing this redirection.

When implementing APIs, we do not care about SEO. So, if our API works both ways, we can let the consumers choose how to use it 🙂.

To perform these redirections in .NET Core, we could use the URL Rewriting Middleware with one of the following regular expressions (regex):

Regex Goal Regex String & Match Example Replacement String & Output Example
Strip trailing slash Regex: (.*)/$
Example: /path/
Replacement: ` $1 `
Example: /path
Enforce trailing slash Regex: (.*[^/])$
Example: /path
Replacement: $1/
Example: /path/

Data Filtering, Sorting, and Pagination

In a previous section, we saw that we could use an endpoint such as the https://api.mydomain.tld/products to get all available products. However, the response may be huge depending on the number of available products in our database. So, it would be preferable to request a subset of the available products depending on the user’s preferences. For that purpose, we could provide in our APIs the filtering, sorting, and pagination functionalities.

For the following examples, let’s assume that we have the following products:

[
    {
        "productId": 2,
        "Title": ".NET T-Shirt",
        "Price": 10,
        "Sizes": ["M","L"]
    },
    {
        "productId": 3,
        "Title": "C# T-Shirt",
        "Price": 8,
        "Sizes": ["S","M","L","XL"]
    }
]

Filtering

In our case, apply filtering means limiting the results by specific criteria. To represent that in a URL, we should use query string parameters. The query string is a part of the URL which starts after the question mark “?” character. Each query parameter has the field=value format, and multiple query parameters are separated by the ampersand “&” symbol. For example, the following table presents some possible query strings to filter the JSON example of the previous products.

GET Query String Example Description
/products?price=10 Get all products with a price equals to 10.
/products?minPrice=50&maxPrice=100 Get all products with a minimum price of 50
and a maximum price of 100.
/products?sizes=M,L Get all products with a size of medium or large.

Sorting

To represent the sorting criteria in the URL, we need to define a special query string field name (e.g., sort) that we will use only for sorting. We can determine the multiple property names (e.g., price, title, etc.) used to sort the data in this field. In addition, the plus “+” and minus “-“ characters can be used as a prefix in the property names to define if the sorting should be ascending or descending, respectively.

GET Query String Example Description
/products?sort=+price Sort the products list by ascending prices (smallest to largest).
/products?sort=-price,+title Sort the products list by descending prices and then by
ascending titles (A to Z).

Pagination

Filtering can limit the response data, but the user’s choices determine the limitation. However, in pagination, the user usually gets a specific number of items (a first subset of the data) and can navigate to the next and previous pages (subsets).

In our APIs, we can limit the amount of data returned in a single request to have fast responses and limit the use of network bandwidth.

To represent the pagination criteria, we can define two special query string fields, limit and offset, to set the maximum number of returned items and the number of them that should be skipped (offset) (Price E. et.al, 2018). The following table presents some query string examples to paginate the products data by five items.

GET Query String Example Description
/products?limit=5 Get the first five products. When we do not set the
offset parameter, we can assume that it has a zero value.
/products?limit=5&offset=5 Get the following five products (limit=5) by skipping
the first 5 (offset=5).
/products?limit=5&offset=10 Get the following five products (limit=5) by skipping
the first 10.

When implementing pagination, the main question is, “How would we know when the data ends?”. There are two solutions to this problem (which can also be combined).

  1. Get the total items (in our case products) in the data response or a custom HTTP header (e.g., with the name X-Total-Count). In this way, we can generate the next, final, etc., URLs and navigation links (HTML <a> tag) on the client-side.
  2. Get the navigation links from the server in the Link HTTP header (Nottingham M. & IETF, 2010). The Link header is a standard way to provide a list of links to the consumer. The basic format of a Link value is <URL>;rel="TheRelationType", which can be separated by a comma (“,”) character when multiple link values exist, as we can see in the following example. Thus, by using the Link header values (URLs and rel), we can generate the navigation links (HTML <a> tag) on the client-side.
Link: <https://api.mydomain.tld/products?limit=5>; rel="first",
<https://api.mydomain.tld/products?limit=5>; rel="prev",
<https://api.mydomain.tld/products?limit=5&offset=5>; rel="next",
<https:// api.mydomain.tld/products?limit=5&offset=10>; rel="last"

Summary

Naming our endpoints based on the REST architectural constraint of identification of resources can be complicated. This article showed some practical suggestions for adopting consistent naming conventions in our URLs (API endpoints) and how we could perform filtering, sorting, and pagination.

The following table summarizes these practical suggestions and tips for naming our URLs and perform filtering, sorting, and pagination.

Practical Suggestion Example(s)
The identification of the resources should be made based on the consumer needs and business logic.  
Avoid creating APIs that mirror the backend implementation or internal structure of our database(s).  
Use plural nouns for referring to resources. - /products
- /products/123
- /orders
Non-resource scenarios, such as processes and executable functions can use verbs (as exception in the previous suggestion). - /cart/checkout
- /server/321/restart
- /search
Decide to either represent the resource relationships in the URL or use flat endpoints depending on the complexity of the resource relationships. Simple Resource Relationships
- /users/5/orders
- /orders?user=5

More Complex Resource Relationships
- /orders/123/products/1
- /order-products?order=123&id=1
It is recommended to use a path that is not more complex than: /collection/item/collection/item.  
Using a trailing slash in API URLs is just a personal preference. For APIs, if both ways are supported, we are all set. On the contrary, we should redirect (301 - Moved Permanently) the non-preferable way to the other. - /products/
- /products
On GET methods use query strings to represent the filter criteria. - /products?price=10
- products?minPrice=50&maxPrice=100
On GET methods use the sort query string to represent the sorting criteria. - /products?sort=+price
- products?sort=-price,+title
On GET methods use the limit and offset query strings to represent the pagination criteria. - /products?limit=5
- /products?limit=5&offset=10
For pagination, read about the X-Total-Count and Link HTTP headers to provide needed information to create the paging navigation URLs.  

If you don’t like these suggestions or have already selected different approaches, please share them with us in the comments. The important thing is to be consistent with your chosen URL and query string naming conventions to help your API consumers.

References

If you liked this article (or not), do not hesitate to leave comments, questions, suggestions, complaints, or just say Hi in the section below. Don't be a stranger 😉!

Dont't forget to follow my feed and be a .NET Nakama. Have a nice day 😁.