Please note that this post is a work in progress, because there is a a lot of ground to cover. Shouldn’t take more than a week to flesh the whole thing out. Thanks for your patience.


This post outlines my playbook for designing a RESTful API service’s endpoints (method + URL) and return values (HTTP status code and body). It’s organized by scenario so if you know what your goal is, you can jump straight to it.

This is merely a scaffolding that I like use. It attempts to maintain as much consistency in organizing endpoints and return values as possible in order to make life easy for both the API developer(s) and the client developer(s).

It is important for me to state that I am not saying that this is the way to build a RESTful API. Instead, this is a convention or guidebook of sorts that may help you overcome some thorny scenarios. I’ve compiled it from building and consuming APIs myself over the years, as well as from working with people actively consuming APIs I’ve built.

Before I begin, I highly recommend you peruse Vinay Sahni’s Best Practices for Designing a Pragmatic RESTful API for general RESTful goodness. His post covers various topics on building a RESTful API that you may find useful.

Okay, to keep this organized there are two sections. The first is a list of guiding principles that have informed my solutions. The second section is a series of responses to common RESTful scenarios that define the schema for the API inputs and outputs.

Contents

Here’s a table of contents in case you know what issue you’re trying to solve and want to jump straight to it:


1 Principles

1.1 PUT is idempotent

When an endpoint is idempotent, calling the endpoint multiple times in the exact same manner will result in the same outcome as calling the endpoint once. Updating the name of a user to “Tom”, if done over and over again, will always result in the user’s name being “Tom”.

According to the HTTP/1.1 spec:

the side-effects of N > 0 identical requests is the same as for a single request

The spec also dictates that PUT requests are to be idempotent. The schema outlined below is designed to honor this specification.

1.2 POST can be idempotent

I have read a lot of comments on StackOverflow over the years arguing that a PUT request should be made instead of a POST request because the endpoint will be idempotent and the HTTP/1.1 spec says PUT requests are idempotent.

Yes, the spec does say PUT request must be idempotent, as well as GET, HEAD, DELETE, OPTIONS and TRACE requests. It does not, however, require that a POST not be idempotent.

The schema outlined below allows POST requests to be idempotent, as this does not violate the HTTP/1.1 spec.

1.3 Collection nouns are pluralized

In a RESTful API, a collection is an endpoint that may return a list of objects. Often, a collection endpoint is filterable, allowing the consumer to search through a collection for objects that match specific criteria.

In endpoint URLs, a collection is expressed with a plural noun. For example, a collection of User objects will be identified by “users” in the URL.

If you want a single object from the collection, provide the ID of the object you want in the URL.

1.4 All root-nodes are collections

A root-node is the first (left-most) object-type referenced in your endpoint URL. If the URL is /users/12/posts, then the root node is users, a collection representing User objects. You can tell it’s a collection because its pluralized, and Principle 3 tells us to pluralize it.

1.5 Single node nouns are singular

Occasionally a node may have a has–1 relationship to another node. A User object might have a single Media object as its avatar, for example. Since the User can only have at most 1 object, the noun representing the relationship can be in the singular form. This tells consumers that the endpoint is not a collection, and that an array of objects will not be returned. The User object endpoint would then be /users/123/avatar, where 123 is the ID of the User object to fetch the avatar for.

1.6 Single-level relationships only

Your API endpoints do not need to allow for second-level relationships.

What’s a second-level relationship? The easiest way to explain this is with people. If you are a node, and your friends are your relationships, then your friends’ friends are the second level, and there’s no reason to account for them in your URL scheme.

So no URLs with 3 or more nouns in them, basically. No /users/123/pictures/345/comments. You should have an endpoint to get Picture #345’s comments, it’s /pictures/345/comments.

It’s not so much that it’s wrong to do it, it’s just that this scheme chooses to keep things simple. While /users/123/pictures/345/comments/57/user is perfectly legal if you want to support it, we’ll not worry about relations-of-relations-of-relations-of-relati…

1.7 If the situation dictates, violate a principle…AND THEN DOCUMENT THE HELL OUT OF IT

Principles are guidelines that we can all generally agree on, or at least abide. That doesn’t make them laws. If you have a compelling reason to violate a principle, such as limits on technology or legacy systems to support, then make your exception.

And then write it down so everyone knows.

Like, everywhere.


2 Scenarios

2.1 Actions on root-collection

The root collection is the first noun in a RESTful API URL. The root collection for User objects is /users, for example. Most tutorials in RESTful API design cover the root collection in detail. It is by far the most straightforward part of the REST convention as it maps so well in concept to CRUD operations on a table in a datastore.

2.1.1 Create node

To create a new node:

URL Schema <base-URL>/[<version>/]<plural-node-name>
Request Method POST
Request Body The body of the request should be the data required to create the node as key/value pairs.
HTTP Code 201
Response Body The body of the response should be the newly created node

Examples:

Node Type Method URL Response
User POST http://yourapi.com/v1/users A User
Category POST http://yourapi.com/v1/categories A Category
Octopus POST http://yourapi.com/v1/octopi An Octopus

Back to Table of Contents

2.1.2 Read single node

To read a single node:

URL Schema <base-URL>/[<version>/]<plural-node-name>/<node-ID>
Request Method GET
HTTP Code
  • Node exists: 200
  • Node does not exist: 404
Response Body The requested node if it exists, or nothing

Examples:

Node Type Method URL Response
User GET http://yourapi.com/v1/users/12 A User
Category GET http://yourapi.com/v1/categories/928 A Category
Octopus GET http://yourapi.com/v1/octopi/7 An Octopus

Back to Table of Contents

2.1.3 Read/filter list of nodes

To read a list of nodes:

URL Schema <base-URL>/[<version>/]<plural-node-name>
Request Method GET
HTTP Code 200
Response Body An array of the requested nodes

Examples:

Node Type Method URL Response
User GET http://yourapi.com/v1/users Array of Users
Category GET http://yourapi.com/v1/categories Array of Categories
Octopus GET http://yourapi.com/v1/octopi Array of Octopi

Back to Table of Contents

2.1.4 Update node

To update an existing node:

URL Schema <base-URL>/[<version>/]<plural-node-name>
Request Method PUT
Request Body The body of the request should be the data to change on the node as key/value pairs.
HTTP Code
  • Node exists: 200
  • Node does not exist: 404
Response Body The body of the response should be the updated node

Examples:

Node Type Method URL Response
User POST http://yourapi.com/v1/users A User
Category POST http://yourapi.com/v1/categories A Category
Octopus POST http://yourapi.com/v1/octopi An Octopus

Note: A truly RESTful endpoint for updating an object would use a PATCH request for updating only the fields provided, and a PUT request for replacing the object entirely to have just the provided fields and values. As PATCH is still poorly adopted in browsers, this convention treats a PUT request as a PATCH request; namely, a “safe” update.

Back to Table of Contents

2.1.5 Delete node

To delete an existing node:

URL Schema <base-URL>/[<version>/]<plural-node-name>/<node-ID>
Request Method DELETE
HTTP Code 204

Examples:

Node Type Method URL Response
User POST http://yourapi.com/v1/users/3 nothing
Category POST http://yourapi.com/v1/categories/467 nothing
Octopus POST http://yourapi.com/v1/octopi/78 nothing

Note: If the node doesn’t exist and you try to delete it, should it through a 404? This convention argues no: if the goal was to remove the node, and it happens to not exist already, then no harm, no foul. Additionally, DELETE operations should be idempotent, so subsequent DELETE requests should not get 404 errors after the first request returns a 204 no-content success.

Back to Table of Contents


2.2 Actions on a relationship where there is only a single child (1-to–1, parent-to-child)

When working with a 1-to–1 relationship, the root node will only ever be related to a single other node within the relationship. As a result, the relationship does not represent a collection, and so will not ever return an array of objects. To denote this individual relationship, the name of the relationship is a singular noun.

As is the case with any relationship, the noun used to identify the relationship (generally the noun that follows the root-node noun), can be either the name of the object the root-node is related to, or a descriptive name for the relationship.

2.2.1 Create child node (and attach to parent)

To create a child node and simultaneously attach it to the parent node:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<singular-child-node-name>
Request Method POST
Request Body The body of the request should be the data required to create the child node as key/value pairs.
HTTP Code 201
Response Body The body of the response should be the newly created node

Examples:

Parent Child Method URL
User Media POST http://yourapi.com/v1/users/12/media
User Media POST http://yourapi.com/v1/users/365/avatar
Post Tag POST http://yourapi.com/v1/posts/93/tag

Back to Table of Contents

2.2.2 Attach child node to parent node

To attach a child node to the parent node:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<singular-child-node-name>/<child-node-ID>
Request Method POST
HTTP Code 201
Response Body The body of the response should be the child node

Examples:

Parent Child Method URL
User Media POST http://yourapi.com/v1/users/12/media/46
User Media POST http://yourapi.com/v1/users/365/avatar/72
Post Tag POST http://yourapi.com/v1/posts/93/tag/3

Back to Table of Contents

2.2.3 Read child node of parent

To read the child node attached to the parent:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<singular-child-node-name>
Request Method GET
HTTP Code
  • If there is a child: 200
  • If there is no child: 404
Response Body The body of the response should be the child node, if there is one

Examples:

Parent Child Method URL
User Media GET http://yourapi.com/v1/users/12/media
User Media GET http://yourapi.com/v1/users/365/avatar
Post Tag GET http://yourapi.com/v1/posts/93/tag

Back to Table of Contents

2.2.4 Detach child node from parent

To detach the child node attached to a parent:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<singular-child-node-name>
Request Method DELETE
HTTP Code 204

Examples:

Parent Child Method URL
User Media DELETE http://yourapi.com/v1/users/12/media
User Media DELETE http://yourapi.com/v1/users/365/avatar
Post Tag DELETE http://yourapi.com/v1/posts/93/tag

Back to Table of Contents


2.3 Actions on a collection of children (1-to-many, parent-to-children)

Acting upon a collection of a root-node’s children is another very common use case in RESTful API tutorials. More often than not, however, the tutorial describes a many-to-many relationship, or assumes a 1-to-many and a many-to-many should be treated the same way. There’s nothing inherently wrong with doing so, but having conventions for each case separately does help to keep things consistent. A many-to-many relationship might not allow for simultaneously creating a related node and attaching it to the parent, but to 1-to-many relationship might.

The key difference between the two is this: are the two nodes being related peers, meaning they can exist and function when not attached, or are they in a parent-child relationship where one depends upon the other to exist? This section deals with the latter.

Like any relationship, the URL begins with the root-node’s collection URL, meaning the plural form of the root-node, and includes the plural form of the child-node’s name, or the plural form of the name of the relationship.

2.3.1 Create child node (and attach to parent)

To create a child node and simultaneously attach it to the parent node:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<plural-child-node-name>
Request Method POST
Request Body The body of the request should be the data required to create the child node
HTTP Code 201
Response Body The body of the response should be the newly created node

Examples:

Parent Child Method URL
User Media POST http://yourapi.com/v1/users/12/media
User Post POST http://yourapi.com/v1/users/365/posts
Post Tag POST http://yourapi.com/v1/posts/93/tags

Back to Table of Contents

2.3.2 Attach child node to parent node

To attach a child node to the parent node:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<plural-child-node-name>/<child-node-ID>
Request Method POST
Request Body The body of the request should be the data required to create the child node
HTTP Code 201
Response Body The body of the response should be the child node

Examples:

Parent Child Method URL
User Media POST http://yourapi.com/v1/users/12/media/46
User Post POST http://yourapi.com/v1/users/365/posts/72
Post Tag POST http://yourapi.com/v1/posts/93/tags/3

Back to Table of Contents

2.3.3 Read specific child node of parent

To read a specific child node attached to the parent:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<plural-child-node-name>/<child-node-ID>
Request Method GET
HTTP Code
  • If the child exists and is attached: 200
  • If the child does not exist: 404
  • If the child is not related to the parent: 404
Response Body The body of the response should be the child node, if there is one

Examples:

Parent Child Method URL
User Media GET http://yourapi.com/v1/users/12/media/82
User Post GET http://yourapi.com/v1/users/365/posts/14
Post Tag GET http://yourapi.com/v1/posts/93/tags/6

Note: The result of this endpoint functions very similarly to just getting the child object directly as explained in 2.1.2 Read single node. The key difference is that this endpoint will raise an error if the specified child is not related to the parent.

Though the 404 response could mean one of two things, remember that the endpoint is for a relationship, and the 404 here means that a relationship could not be found. If the child doesn’t actually exist, then no relationship could exist, and the 404 is an appropriate response.

Back to Table of Contents

2.3.4 Read/filter list of child nodes of parent

To read a list of child nodes attached to the parent:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<plural-child-node-name>
Request Method GET
HTTP Code 200
Response Body The body of the response should be an array of child nodes

Examples:

Parent Child Method URL
User Media GET http://yourapi.com/v1/users/12/media
User Post GET http://yourapi.com/v1/users/365/posts
Post Tag GET http://yourapi.com/v1/posts/93/tags

Back to Table of Contents

2.3.5 Detach child node from parent node

To detach a child node from the parent:

URL Schema <base-URL>/[<version>/]<plural-parent-node-name>/<parent-node-ID>/<plural-child-node-name>/<child-node-ID>
Request Method DELETE
HTTP Code 204

Examples:

Parent Child Method URL
User Media DELETE http://yourapi.com/v1/users/12/media/88
User Post DELETE http://yourapi.com/v1/users/365/posts/17
Post Tag DELETE http://yourapi.com/v1/posts/93/tags/539

Back to Table of Contents