More Bang for Your Buck

Defining an API is one of the hardest challenges a software engineer has to go through. You’ll invariably have to take several guesses in regards to how the API will be used and how it will evolve, making it as future-proof as you can, or risk having to redo it with a v2 prefix and doubling your maintenance effort.

I’ve read a lot of posts on the art of API-defining but I haven’t ever read one which proposes a method, which doesn’t always work but perhaps should be implemented when it can, and which is skipping the challenge altogether and not defining an API.

Here’s what I mean.

Let’s consider the most basic GET request on an MVC-style server-rendered application. There’s essentially three steps.

  1. A request is made to /resource/1.
  2. The controller fetches the model with ID 1 from the database.
  3. The view takes the model and uses it to render whatever is needed.

Now, here’s the same thing in a client-rendered application.

  1. A request is made to /resource/1.
  2. A bundle is downloaded which includes code to both render the view and download the data needed to render it from a well-defined API endpoint.
  3. A request is made to /api/resource/1.
  4. The resource with ID 1 is serialized into a well-defined schema and returned to the client.
  5. The client uses the resource to render the view.

Pushing aside discussions of performance and whatnot, it’s clear that we had to do more development work on the second flow. We had to sit down and define an endpoint and a schema for a resource that’s returned from that endpoint while on the first flow we didn’t have to do any of this at all.

Here’s another, perhaps more controversial, example.

Our authentication system is composed of a User model and an AccessToken model. At some point we’re required to support a new authentication flow based on passkeys and hence decide to introduce a new PassKey model.

Solution number one is just placing the PassKey model at the same level as the AccessToken one. Same thing for the logic itself.

Alternatively, we could modularize.

Should we move all models to the auth package? Maybe just move the models that are directly related to authentication to the new package and keep User in app. But then we can’t be having app call into auth and auth into app because that introduces a circular dependency which is bad design. So let’s make auth into a library and introduce a well-defined API.

We just put ourselves up for more work in the name of modularization, expandability and “better” software design and yet have reached the exact same end result.

Now, before you curse me to the fiery pits of software development hell let me be very clear: there’s good reasons for having to go through this work.

If scale requires it, you’ll have completely separate teams handling frontend and the backend codebases. Or you’ll have a team dedicated to handling authentication code. At this point you’ll need to be spending time building APIs.

But this is not the case for a good chunk of software companies. So, until you really, really can’t why not just do the simple thing?