Avoiding Tightly-Coupled REST APIs

As you might have read, WordPress is getting a powerful REST API. For a few years now I’ve been writing endpoints for the version of this API, as well as few REST APIs in other languages (Ruby, JavaScript). As I’m also a big fan of unit testing, I’d like to share a pattern that I’ve learned from trial and error.

To make REST API endpoints more testable, as well as more useful, write the logic in a separate library.

Let’s say I have an endpoint that updates a menu item on a restaurant web site. In WordPress I have the menu item set up as a custom post type (you don’t need to grok WordPress to benefit from this pattern, but I thought I’d mention it).

The basic operation of the endpoint is this:

  1. Take the post ID and the new field values from the data submitted to the endpoint.
  2. Find the matching post in the database.
  3. Verify user permissions to update that post.
  4. Calculate the menu item’s total price based on current tax rate and ingredients.
  5. Update the post’s data.
  6. Return the newly updated post data to the client.

I’d also like to write unit tests for each of the above operations.

Now, the first step in writing my endpoint would be to map the HTTP route to the route handler. That is, I need to map the route PUT /menu/item to a function which kicks off the process above and eventually returns the result to the client.

I could write all of the above logic right in that function. There’s even a few pieces which could be their own separate functions. But this way may not be ideal, as I’ll explain.

The route handler, wherever it might be defined, is aware that it’s part of a REST API. The exact details of that vary depending on which language, architecture, etc, that you are using. But generally the initial route handler is not just doing the work of the steps above, but also interacting in some way with the API. Perhaps it’s checking details of the path, query string, user token, or it’s sending back an HTTP-specific response.

Here’s where we can get into trouble. Let’s say later on we decide to write another REST API endpoint that accepts a new tax rate and updates all the menu items. It needs to do the following:

  1. Take the new tax rate from the submitted data.
  2. Iterate over each menu item post.
  3. Validate user permissions to update each post.
  4. Calculate each menu item’s new total price.
  5. Update each post.

Notice that steps 3, 4, and 5 are the same as the previous endpoint we made. Rather than re-write them here, let’s keep things DRY and just call the other endpoint; thus our new update-the-tax-rate endpoint looks more like this:

  1. Take the new tax rate from the submitted data.
  2. Iterate over each menu item post.
  3. Call the update-menu-item endpoint.

Ah… but in the WordPress REST API, as well as many other API frameworks in different languages, you can’t call an API endpoint from within another API endpoint. And it might be said that, even if you could, doing so adds unnecessary overhead.

In this case, if the process of finding, validating, and updating a post was all one function call in a library, maybe update_menu_item(), then this endpoint becomes so much easier. Both the update-menu-item endpoint and the update-the-tax-rate endpoint can just call update_menu_item().

Even better, so can our unit tests! While it’s a good idea to write tests for an API endpoint that actually call the API, doing so can sometimes be problematic. Unit tests usually try to test one specific behavior, separating that behavior from the rest of the system. If you test a REST API, the system being tested is on a separate server (even if the server is local to your computer), and is thus much more complicated to control. But if the API’s route handler simply calls a function in a library, we can just test that function, providing mock data and checking the results, all without even considering HTTP.

Certainly this isn’t a rule, as most rules in programming tend not to fit real-life situations. It’s just a pattern I’ve decided works well for me. I suggest you examine your own code to see if it might work for you too!

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s