RESTful APIs are a well known concept in API design. They allow developers to leverage HTTP URLs and verbs to create versatile interfaces for CRUD operations as well as searching and filtering.
As a small recap, here are the HTTP verbs and what they are mapped to conceptually:
Verb | URL | Body | Purpose |
GET | /items | - | Retrieve all items |
GET | /items/123 | - | Retrieve item 123 |
GET | /items?owner=johndoe | - | Retrieve all John Doe items |
POST | /items | { "owner":"johndoe" } | Add a new item |
PUT | /items/123 | { "owner":"johndoe", "color": "red" } | Replace the entire item 123 |
PATCH | /items/123 | { "color":"cyan"} | Update only part of the item 123 |
DELETE | /items/123 | - | Delete item 123 |
So far so good. Now let's get things a bit more complicated: how can I achieve goals like mass update and mass delete?
Verb | URL | Body | Purpose |
PATCH | /items?color=green | { "color":"cyan"} | Update green items to cyan |
DELETE | /items?color=black | - | Delete all black items |
DELETE | /items | {ids: [1,2,3,...]} | Delete all items with the specified ids |
Note how the update action can only be implemented as a PATCH
. If we need to replace all items matching a query parameter, we're in trouble and need to get fancy, stepping outside the pure REST world.
Another example of hitting the REST boundaries are the implementation of complex searches. Within a GET
request, a query parameter or two is usually nothing more than a simple filter, but what if we need more convoluted search mechanism? We would ideally need something similar to this request:
Verb | URL | Body | Purpose |
GET | /items | {"condition1":{"and": { "field1": { "equals": "value1"}, "field2":{"gte": 5}}}} | Find all items matching condition1 |
There are two problems here:
While allowed to any HTTP request, adding a body to a GET request is a bad idea.
We hit the same endpoint with the same verb to perform a simple filter (or even a get all) and a complex query. While similar, those are different behavior, with the latter way more complicated than the former as the server has to parse and understand a custom query language.
So what is the solution? Firstly, we don't need to think RESTfully every time. Complex searches are outside the REST goals, thus we need to be fancier and create, for example, our own query language or convention. Secondly, we can create a REST-ish boundary between REST and non-REST world. That is:
Verb | URL | Body | Purpose |
POST | /items/search | {"condition1":{"and": { "field1": { "equals": "value1"}, "field2":{"gte": 5}}}} | Find all items matching condition1 |
This way we clearly separate the search endpoint from the REST ones and make our fancy mess. We can push this one step further by condensing common searches into aliases.
Verb | URL | Body | Purpose |
POST | /items/xmas-things | - | Find all x-mas items |
Conclusion
This is a case where developers have to think a bit out of the box. What matters the most is a clean way of solving a problem, even if we need occasionally to step outside our proudly designed REST API.
To the next bite!