Web Applications: Routes and Best Practices In REST-land

4 min read

REST-based routes for APIs and web applications have existed for twenty-ish years at this point. They are a common method of interacting with web services, especially those that are five to ten years old as they predate Facebook’s GraphQL and are a replacement for cumbersome XML-RPC APIs. However REST-based routes should have an expectation of immutability as should all APIs to some extent.

Let me step back and explain my thought process: as one of the 2019 release managers for Sanic (an asynchronous Python REST API oriented web framework), I get caught up in feature requests that sometimes need an arbiter or a voice of reason, and sometimes just as someone who has to say ‘no’ in order to drive conversations forward. In sharing that, I will also share that the community decided to deprecate a feature: route removal.

I do not think I ever explained to the Sanic community about why I think route removal is bad, but I should also say that I think dynamic route creation is bad too. I am going to take this opportunity to share why I think route modification during run-time is a poor idea.

So why, in my opinion, is route modification during run-time bad?

All APIs should be somewhat self-documenting. In the case of a simple REST-based API, let us assume we have the following routes:

  • GET /user returns JSON user list
  • GET /user/[int:id] returns JSON user object
  • GET /user/[int:id]/[str:element] returns JSON element of user object

These routes are instantiated or created when the application starts, and they exist for the lifetime of the application’s run. Each returns specific information, even though the information itself could change based on the parameters supplied in the route. Assume also for our purpose that security controls around these routes are completely unnecessary (a pleasant fantasy, right?)

In this case the first route would probably return a list of users, while the second route would get a specific user, and the third route would return something about the user, say the name. This should make a solid amount of sense, right?

A client consuming this API, whether it is a browser-based client using an HTTP request, or another service somewhere, or perhaps a fatter client like a mobile app, should have a reasonable expectation that the routes/paths stay consistent. Every time some piece of code asks my API for /user/123/name, for example, the expectation is that route returns user 123’s name.

Now let us move on and make the assumption that we have some strange trigger in place and for whatever reason we remove the user element route from the server while the application is running. How do the clients know? We have to build error handling that specifically knows how to handle it when that route is no longer available. Good code hygiene says that will be done anyway, but alternatively we would have to advertise to our clients through some sort of notification: either WebSocket or maybe a server push if our client is HTTP/2 compliant, or some other option like polling another route that tells us what routes are active.

We will now go further down the path and say we have changed the functionality of the route and dynamically added it back in. Now our element route returns something different. For whatever reason, our developer is returning binary images. How do the clients handle that? They also now have to know about the new return values, and the client has to be able to anticipate that the route has been changed so that it can be handled properly.

If this all seems to you like it is compounding the effort required to handle a route, you would share my opinion. Now what if you are not doing that once, but you’re swapping out the routes entirely? Delete all your existing routes, create new routes, and then the client has to know.

That is patently ridiculous and creates a truly untenable application. In REST API-land, which is still quite prevalent no matter what the GraphQL true believers would tell you, we can version the routes, prefacing everything with /v1.0 or /v2.19 or whatever so that the client knows what to expect when consuming the route, but changing routes on the fly is a recipe for disaster and if not a worst practice, certainly a bad one.

I started out by mentioning that with Sanic, we are deprecating route removal. If routes need to change, the application should be restarted or reloaded, not updated on the fly, and existing routes should stay consistent so that consuming clients understand.

There is an implicit agreement between clients and a service that a route will continue to return the same expected result. Therefore it adds needless complexity to consuming routes for clients, whether those are direct consumption or other services, to change routes dynamically no matter if it is removing, altering, or adding new routes.

Finally, I asked for review of this article to from a trusted source to make sure I was not insane or off-base and was bluntly told that REST is an antiquated way of handling APIs. I do not disagree, but many people are still using REST because they must. Many modern options solve the problem of mutating APIs inherently - gRPC and Thrift, for example, especially when handling microservices that must communicate with each other.

REST will not disappear from the web soon, but APIs and routes must follow best practices, including immutability, in order to expect to be usable for any length of time.

Image of Stephen Sadowski

Stephen Sadowski

Leader focusing on quality, delivery, technical debt management, and leadership education about DevOps and SRE practices