URL Design

URL best practises to design resource identifiers

HTTP URLs are the main point of the REST API design. The structure of URLs therefore have a scheme that you should follow, if you want to develop a good HTTP API.

URLs are also what is visible first later - for example in documentations - and thus can lead to a first judgment about an API quality. The better and more standardized the structure of the URLs, the better the API will be received by developers.

The creation of a URL design is one of the most important foundations of an API. This should be implemented very carefully as part of an architectural approach.
Every change has a direct impact on the clients and represents a breaking change.

Naming, Collections and Nouns

The purpose of a URI is to uniquely address resources. Accordingly, it is the common way and recommendation that the URI sections are identifiers of resources. The structure should therefore be designed in such a way that even a human can navigate through the data.

So if you want to see what tasks there are, the base would be /tasks.
The API would then respond with a list of tasks.

// URL: /tasks
[
    {
        "id": 123,
        "title": "Buy flowers",
        "isDone" : false
    },
    {
        "id": 789,
        "title": "Water flowers",
        "isDone" : false
    },
    
    ... more tasks here
]

The list of resources (here tasks) should always contain the identifier of the respective object, here the Id.
The Id is now used to uniquely identify the individual address via the URI, here for example /tasks/123.

// URL: /tasks/123
{
    "id": 123,
    "title": "Buy flowers",
    "isDone" : false
}

Actions are HTTP Verbs

Verbs represent actions on resources that are implemented in REST via HTTP Request Methods.

HTTP Verb CRUD Examples Action description Behavior
GET Read /tasks, /tasks/123 Query an entire collection or an individual resource If a collection or a resource is found, the response is given together with status code 200 (Ok), otherwise 404 (Not found).
POST Create /tasks Passes a new resource to a collection Often status code 200 (Ok) is returned with the create resource as body. Asynchronous APIs often respond with status code 201 (Created) and the id of the created resource or an operation. If the collection does not exist, the answer is 404 (Not found).
PUT Update/Replace /tasks/123 Replaces an existing resource The updated resource is used as the response body together with status 200 (Ok). If the resource does not exist, the response is given with status code 404 (Not found). If a resource cannot be updated, the response is 409 (Conflict) or status code 405 (Not Allowed) if updating is not allowed.
PATCH Update/Modify /tasks/123 Updates an existing or a part of an existing resource The updated resource is used as the response body together with status 200 (Ok). If the resource does not exist, the response is given with status code 404 (Not found). If a resource cannot be updated, the response is 409 (Conflict) or status code 405 (Not Allowed) if updating is not allowed.
DELETE Delete /tasks/123 Deletes an existing resource The deleted resource is used as the response body together with status 200 (Ok). If the resource does not exist, the response is made with status code 404 (Not found). If a resource cannot be deleted, the response is 409 (Conflict) or with status code 405 (Not Allowed) if deletion is not allowed.

Due to the different verbs, a single URI endpoint is able to offer different actions, respectively different returns.

  • POST on /tasks adds a new task to the collection.
  • GET on /tasks returns all existing tasks in the collection.

It is therefore extremely important to implement and use the verbs correctly.

Hierarchical objects

Usually resources are not as simple as in this task example, but have relations to other resources or have further objects as part of their hierarchy.
So let’s assume that instead of the task we have an article, it might look like this:

// URL: /article/123
{
    "id": 123,
    "createdOn" : "2022-01-27T108:27+03:00",
    "modifiedOn" : "2022-02-03T16:47+18:00",
    "title": "REST is nice! Use it now in your public HTTP API!",
    "text" : "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
    "textType" : "PlainText",
    "author" : {
        "id" : 3105,
        "name: "Benjamin Abt",
        "mail" : "mail@benjamin-abt.com",
        "nickname" : "Ben",
        "twitter" : {
            "url" : "https://twitter.com/abt_benjamin",
            "username" : "abt_benjamin"
            "avatarUrl" : "https://pbs.twimg.com/profile_images/1404387914344243203/oUO_LNAp_400x400.jpg"
        }
    },
    "tags" : ['.NET','Azure','API','REST'],
    "comments" : [{
        "id" : 8758,
        "text" : "orem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam",
        "on" : "2022-01-29T16:47+01:00",
        "by" : {
            "id" : 789,
            "name" : "Batman"
        }
    },{
        "id" : 485,
        "text" : "orem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam",
        "on" : "2022-01-30T16:47+01:00",
        "by" : {
            "id" : 325,
            "name" : "Robin"
        }
    },{
        "id" : 63789,
        "text" : "orem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam",
        "on" : "2022-01-29T16:47+01:00",
        "by" : {
            "id" : 245,
            "name" : "Catwoman"
        }
    }]
}

However, it should be noted:

  • The hierarchical structure is now given by the embedded objects like author or even comments, which in turn have their own hierarchical structure.
  • There is no fixed rule that this structure is directly addressable by the URL, but it is quite common. However, the resources themselves should be individually addressable. The hierarchical structure of the author from the point of view of the article is: /articles/123/author. However, the author should also be addressable with /authors/3105.
  • The id results from the hierarchical object (in article), which is why the id always should be part of a resource. However, the hierarchical resource return (/articles/123/author) may be different from the identifier url resource return (/authors/3105).

Filtering

Filtering of returns should always be part of an API. However, REST does not have a standard at this point.
However, it is common to be able to filter based on properties of the resource. For example, based on the id.

/articles?id=123

Filtering

Pagination is especially in request when dealing with very large lists, or when an API is not supposed to give out all the data at once. The usual identifiers for the limited returns are skip and take, or limit and offset.

Query Result
/articles?take=10 returns the first 10 articles (index 0 to 9)
/articles?skip=20 skips the first 20 articles (index 0 to 19) and returns the article collection starting from index 20
/articles?take=10skip=20 skips the first 20 articles (index 0 to 19) and returns 10 starting from index 20

As can be seen, the take parameter in this case is Optional, which requires that the server knows a default (e.g. 10) if the client does not transmit a value at this point. The alternative would be to declare this value as mandatory. However, besides a default, it is also important to limit the values themselves, so that, for example, a maximum of 100 elements are returned, no matter what the client specifies.

Additionally, it is also important to note that the client itself must know how many resources are in a collection in order to be able to calculate the number of pages. This value can be transmitted either via the header, or as part of the body. In the second case, the general structure of the response must be modified: we have to add a metadata object.

{
    "data" : [ // collection of articles
        {
            "id": 123,
            "createdOn" : "2022-01-27T108:27+03:00",
            "modifiedOn" : "2022-02-03T16:47+18:00",
            "title": "REST is nice! Use it now in your public HTTP API!",
            "...."
        },
        {
            "id": 456,
            "createdOn" : "2022-01-27T108:27+03:00",
            "modifiedOn" : "2022-02-03T16:47+18:00",
            "title": ".NET is a great platform to use!",
            "...."
        }
    ],
    "metadata" {
        "take" : 10, // articles to take
        "skip": 20, // articles to skip
        "total" : 738 // total numbers of articles, so we know we have 74 pages each 10 articles in total (last page 8)
    }

Security

When it comes to security, special care must be taken to ensure that no information bypasses the HTTPS encryption. This means that all sensitive content must be transmitted either via HTTP headers or via the HTTP body. The query values are not part of the encryption and can be seen from any point in the data transmission despite HTTPS!

See Security for details.

Versioning

Versioning is always a topic of discussion, since there is not only one way here either.
The general possibilities are:

  • HTTP URI (/api/v2/articles/...)
  • HTTP query parameter values (/articles?version=2)
  • HTTP header values

There are pros and cons to all three, but by far the most common and general recommended way is to version via the URL.

See Versioning for details.