Dmitry Shpika // scriptin
type Links = GitHub | LinkedIn | YouTube | StackExchange | NPM;

RESTful HTTP: organizing URLs

Published on 26 Jul 2014

5 mins read

For the last few week I’ve been working a lot on designing/implementing some web services, mostly RESTful, so I decided to write down the most important things I’ve learned so far. This post is about organizing URLs of resources in a web service in a smart way.

Few basic things before I start:


In REST terms, the state (data) provided and managed by a web service is organized into resources (units of state). For example, URL http://example.com/products may be a resource which is a list of products, while http://example.com/products/123 is a resource representing a single product.

Each resource has an URL (URI actually, but since I talk about HTTP, those two are interchangeable), and those URLs usually form a logical hierarchy.

There are two common types of resources: collections (lists or sets of data) and single items (usually inside a collection).

Organizing collections

Rule 1: each resource should be accessible via only one URL

Consider you have web service with two types of entities: “user” and “group”. Each user can belong to one or more groups. Since logically users are “nested” in groups (groups contain users), you may want to organize your URLs like /groups/123/users/456 - which is user with ID=456 inside a group with ID=123. But the exact same user may be in a different group - /groups/789/user/456.

Bad things may happen when the same resource can be changed or deleted using different URLs. Quick example: how would you know if DELETE /groups/123/users/456 deleted user resource entirely or just excluded it from a group?

To prevent such things, you should leave only one way of accessing each resource - /users/456 in this case. To express the relation between two types of resources, you should use special fields:

{
    "id": 123,
    "name": "Group 1",
    "users": [456, 42]
}
{
    "id": 456,
    "name": "John Doe",
    "groups": [123, 789]
}

Now to add/remove user to/from a group, you can update (using PUT request) either "users" list in a /group/XXX resource or "groups" list in /users/YYY resource - whatever works better.

This brings us to the next rule:

Rule 2: if two resource types have many-to-many relation, don’t nest them. Make two separate resources on the same level of hierarchy

This will help you follow Rule 1.

Example with groups and users above demonstrate many-to-many relation, so it’s better to make users and groups separate instead of putting users inside groups or other way around.

This rule is not applicable to situations when resources form a proper tree (because in that case there is no many-to-many relation), so be careful. For example, if each user could be in only one and exactly one group, it would be perfectly fine to nest users resource path inside the groups resource path.

Moving item from one collection to another

RESTful service should use proper HTTP methods: GET to read, POST to create, PUT to update, and DELETE to remove resources. Consider the case when user can be in only one group - what HTTP request would move the user to another group?

Example: there is a user /groups/123/users/456 and you want to move it to /groups/789, so it becomes /groups/789/users/456 or, if preserving user ID is not important, /groups/789/users/X, where X is auto-generated ID.

Option 1: GET-DELETE-POST - not recommended

This is very simple because no additional methods need to be supported.

GET /groups/123/users/456    - get the data
DELETE /groups/123/users/456 - delete old resource
POST /groups/789/users/      - create new resource with new ID

I do not recommend this approach because it is not atomic. If something goes wrong after DELETE request, the data can be lost forever.

Option 2: WebDAV MOVE method

MOVE method of WebDAV is nice because it uses only 1 request, so it’s atomic if server performs this action atomically.

Request:

MOVE /groups/123/users/456 HTTP/1.1
Host: www.example.com
Destination: http://www.example.com/groups/789/users/456

Response:

HTTP/1.1 201 Created
Location: http://www.example.com/groups/789/users/456

I will not discuss WebDAV approach in detail, this is just information to start with. Read the RFC 4918 if client support issues are not relevant to your case.

Further reading