Taking REST beyond CRUD

Last updated: 08 May 2023

Today’s REST-based APIs have had a positive impact on both developers and business. We have moved beyond complex, expensive stacks of middleware and to a lightweight, easier to integrate approach using REST and CRUD-based patterns.

On the surface, it appears that CRUD and RESTful architecture styles map well together. But is this always true? As you dig further, you’ll find that by only using CRUD-based patterns with your REST APIs, you are limiting your use of REST to a very narrow scope.Let’s examine the “rest” of what REST APIs have to offer for our API toolbelt.

In the beginning, there was RESTful CRUD

CRUD stands for Create, Read, Update, and Delete, which are four primitive database operations. At first glance, these operations map well to the HTTP verbs most frequently used in REST:

Create (SQL INSERT)  : POST - Used to support the creation of a child resource, but can also modify the underlying state of a system.
Read (SQL SELECT)    : GET - Retrieve a representation of a resource, but with additional semantics available.
Update (SQL UPDATE)  : PUT - Update a resource using a full representation. Can also be used to create a resource. The full representation requirement is a large caveat, see the following.
Update (again)       : PATCH - Update a resource using a partial representation.
Delete (SQL DELETE)  : DELETE -  Delete a resource. This is the best matched mapping.

If we stopped the discussion here, you would be able to build an API that allows common operations (Create, Read, Update, and Delete) on single resources or a list of resources. Advanced operations or processes are not yet possible, except as constructed by the consumer of the API.

So when is a CRUD-style approach sufficient? One example would be the case of an API that only operates on a single type of resource, or disparate resources which do not relate. Simple data-driven operations, such as the creation of logs, which are only read and may never be updated or deleted, are one such instance. The fear, however, is that the API may grow in scope. When that happens, heavy lifting, such as relationship management between objects, or transactions, are placed outside the system and under the responsibility of the API user.

Don’t feel discouraged, though. A narrowly-scoped API is sufficient for a large number of projects and products. Many software projects actually exist to enable this exact style of architecture. Examples include psql-api and PostgREST. But, as mentioned before, we can do better for our APIs!

Using the “rest” of what REST has to offer

As mentioned before, we could limit an API exclusively to CRUD operations and be happy with our results a fair amount of the time. So what are we missing, or leaving on the table, when we do this? Here is a short list of considerations that a full-featured REST API can encompass:

  • Processes
  • Relationships between resources
  • Side-effects
  • Changes of state

If instead we treat resources as complex abstractions of business objects, we can open up the capabilities of our REST API to a much wider world.

Processes

Consider the process of ordering a meal at a restaurant. What may seem like a simple, step-based process (order, wait, receive, pay), is actually composed of many micro-steps and processes under the covers. The order process alone consists of waiting for the server to take your order while the initial pleasantries, bread, and water are handled, followed by returning to kitchen, communicating the order to the kitchen staff, and then entering the order into the order management system for later payment.

All of these order-placing details are otherwise hidden under an innocuous-looking POST:

curl -X POST -d '{"entree": "pepperoni_pizza", "drink": "diet_coke", "sides": ["french_fries"]}' https://localhost/guest/1/orders

Many of these business processes could have dependencies that need to be checked and met before a step can process. Even with all of actions completed, we haven’t even reached the point of preparing ingredients and cooking your food! When we take a resource-focused approach to API development, processes and the business objects that they involve are baked into the design.

Relationships between resources

The spirit of REST’s resources mirror the spirit of the web. Specifically, they should express relationships between objects, and provide the means to traverse them by hyperlink. HATEOAS explains further the ways that one can leverage this design to provide navigation guidance for a client through a system’s workflow. Ideally, hypermedia, not arbitrary URL schemes, would represent these relationships.

An example of managing resource relationships using hypermedia, in the context of our earlier restaurant example, featuring an order and items that make up that order:

{
    "id": "d359bd67-8ca4-43f8-87cb-4baa2d806ea8",
    "links": [
        {
            "href": "/orders/d359bd67-8ca4-43f8-87cb-4baa2d806ea8",
            "rel": "self"
        },
        {
            "href": "/orders/d359bd67-8ca4-43f8-87cb-4baa2d806ea8/items",
            "rel": "items"
        }
    ]
}

From the order URL, a client can nagivate to a list of menu items making up that order. Each item would include links to the ingredients needed to create the item, along with a process description for preparation and cooking.

Side-effects

Another advantage we gain by treating resources as business objects is handling side-effects on behalf of, and unseen by, the client. In REST, POST is used for an action which results in a resource creation or side-effect, and can be repeated with the intent of the creation or side-effect happening again. A CRUD Create, on the other hand, explicitly creates a new resource, and cannot be executed a second time without error or data inconsistency.

If a restaurant guest wants to place the same order multiple times, it is not a problem:

curl -X POST -d '{"entree": "pepperoni_pizza", "drink": "diet_coke", "sides": ["french_fries"]}' https://localhost/guest/1/orders
{
    "id": "d359bd67-8ca4-43f8-87cb-4baa2d806ea8",
    ...
}

curl -X POST -d '{"entree": "pepperoni_pizza", "drink": "diet_coke", "sides": ["french_fries"]}' https://localhost/guest/1/orders
{
    "id": "43e10b3b-c1f8-4336-a2f2-a70193fd10a9",
    ...
}

curl -X POST -d '{"entree": "pepperoni_pizza", "drink": "diet_coke", "sides": ["french_fries"]}' https://localhost/guest/1/orders
{
    "id": "43e10b3b-c1f8-4336-a2f2-a70193fd10a9",
    ...
}

While it may seem simpler at first to model an API using CRUD-style data objects, rather than domain or business objects, the drawback is that these considerations are pushed into the hands of the API consumer to manage. This can lead to bugs, or worse, security flaws. From the business perspective, by not abstracting these details away from the user of the API, many customers will be repeating the same work. This could potentially result in increasing the support load of the API owner, as users hit similar obstacles during their course of development.

Changes of state

The biggest advantage to choosing REST over a limited CRUD-driven design is an architectural one. REST gives us the ability to implement a state machine using hypermedia, including contextual capabilities. REST makes the states of a state machine both explicit and addressable – by URIs (Uniform Resource Identifiers). At any time during a workflow, the current state is represented by a.) the URI you just visited and b.) the resource representation you received. The machine’s state can be changed by performing an operation on either the current URI, or a previously linked URI (see hypermedia above), turning either of these URIs into your new state.

Continuing our restaurant analogy, let’s look at the states of operating an internet-connected pizza oven, with resources shortened for brevity:

curl -X GET https://localhost/oven/1
{
    "id": 1,
    "status": "off",
    "time_cook": 0,
    "temperature": 70,
    "rack_height": 2,
    "links": [
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

The pizza oven is off by default, let’s utilize hypermedia and turn it on:

curl -X PATCH -d '{"status": "on"}' https://localhost/oven/1
{
    "id": 1,
    "status": "preheating",
    "time_cook": 0,
    "temperature": 210,
    "rack_height": 2,
    "links": [        
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

The oven is now preheating. Let’s check to see if it is ready:

curl -X GET https://localhost/oven/1
{
    "id": 1,
    "status": "ready",
    "time_cook": 0,
    "temperature": 400,
    "rack_height": 2,
    "links": [
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

Great, we’re ready to cook! Without getting too deep in the weeds on cooking temperature, time, and rack height, let’s assume we proceeded through states “ready”, “cooking”, and “idle”, and we’re now ready to shut off the oven:

curl -X GET https://localhost/oven/1
{
    "id": 1,
    "status": "idle",
    "time_cook": 0,
    "temperature": 400,
    "rack_height": 2,
    "links": [
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

curl -X PATCH -d '{"status": "off"}' https://localhost/oven/1
{
    "id": 1,
    "status": "shutting_down",
    "time_cook": 0,
    "temperature": 380,
    "rack_height": 2,
    "links": [
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

Let’s check one more time, just to make sure we don’t burn down the building:

curl -X GET https://localhost/oven/1
{
    "id": 1,
    "status": "off",
    "time_cook": 0,
    "temperature": 275,
    "rack_height": 2,
    "links": [
        {
            "href": "/oven/1",
            "rel": "self"
        }
    ]
}

Success! We have moved through all the states of cooking a pizza in our internet-connected pizza oven.

As demonstrated, a resource’s representation should include the steps from your current state to all other available states, like the edges and nodes in a graph. This is is the same heritage of using a web browser to navigate the web. REST architecture can work the same way and with the same advantages. This ability to drive a state machine is well beyond the capabilities of a CRUD-based API.

Enlightened REST

Taking a REST API beyond CRUD can yield a wealth of advantages and capabilities: mapping business processes, relationship modeling, side-effect handling, and maintaining a complex state machine. Rather than simply exposing internal database structure, we have the opportunity of modeling services and business logic, while at the same time abstracting away low-level details that can hinder the API consumer. By putting as much thought into your API consumers as your web interface users, you will be well on your way from CRUD interfaces to a more enlightened REST.