Back VS Front-end first when implementing new API calls

Article content

  1. Start
  2. The problem
  3. Leading by example
  4. Front-end first option
  5. Back-end first option
  6. General answer?
  7. Notable exceptions

The problem

When any real-world-production web app starts getting complicated, a very familiar situation comes to all programmers:

Shit I have to something that involves several different API calls. Do I create a new one, or call them separately from the front-end?

On one hand: one server call should (as long as what the server does remains fairly the same) go faster and consume less resources than several ones. As well as reducing the complexity of dealing with several "request-response-error-handling" cycles in the front-end.

But on the other hand: writing more code in the backend tends to increase technical debt, complexity, and the risk of generating inconsistencies in the database or business logic. And it's three times as serious when we talk about writing new code that other parts of the backend were already doing. This could (and should) be minimized and negated by encapsulating, writing helper functions, testing... But we are still ending up with something more brittle and complicated than we started with.

Before answering to when you should which, seeing some ""real"" code should help out.

Leading by example

Let's make a quick real world example. Imagine that we run an application that gives to merchants (our users) an admin to see and update the status of their e-commerce's orders.

This admin allows the merchants to have order statuses (like "On hold", "Dispatched", "Awaiting client info", and so on...).

We would have an API endpoint to change the status of an order like so:

For this example I will be using Ruby language (with some Ruby on Rails methods), just because it's so prose any programmer can quickly tell what is going on

# Back-end server
# This code would be executed when calling the endpoint:
# PUT /api/orders/:order_id/status/:status_id 
# (Note that :order_id and :status_id are dynamic parameters)

order = Order.find(params(:order_id))
new_status = Status.find(params(:status_id)) 
order.change_status(new_status)

Meanwhile, we have another endpoint that sends the order's customer an email with its info and status:

# Back-end server
# This code would be executed when calling the endpoint:
# POST /api/orders/:order_id/email/
order = Order.find(params(:order_id))
order.send_satus_email()

So far so good no?

Now, imagine that in one of the pages of the application the customer wants to change the status of an order AND THEN send the email to its customer

Front-end first option

If we wanted to make it easy for the front-end: we would create a new API endpoint that would:

# Back-end server
# (I don't even the endpoint this would be in)
order = Order.find(params(:order_id))

new_status = Status.find(params(:status_id)) # We passed a "status_id" with the body of the PUT request
order.change_to_status(new_status)

order.send_satus_email()

This would achieve everything we want to do in a single request-response.

But it would put the (poor) back-end engineer in charge of implementing it in a tough position. Should he duplicate the implementation tests done for the other two endpoints? Should he just open the window and throw his "I ❤ DRY" poster out?

Sure, this example is literally 4 lines of code. But it can get pretty messy pretty fast in a real world case. With many more lines and many more method calls, potentially changing more database instances, and thus incremeanting the possible inconsitencies and bugs from having duplicated code.

Back-end first option

We would keep the two endopoints separate and not create any new one.

Meanwhile, the front-end engineer would have to do something like this:

  // Forgive my use of await. It's just to write the simplest possible code and make a point.

  await fetch(`/api/orders/${order_id}/status/${status_id}`, {method: 'PUT' })
  await fetch(`/api/orders/${order_id}/email/`, {method: 'POST'})

In that example I skiped lots of things, like dealing with response, possible error-feedback messages...

But you get the point: to not make the back-end more complicated and off-set the work to the front-end.

General answer?

To my experience: front-end implementations, pages, components, (heck even technologies) come and go into a same project. Rotating way more than the backend, which stays as a single source of truth for way longer.

The back-end should be as simple as possible, so if you have some chance to do a couple more steps in the front-end to avoid re-creating another endpoint or method: do it. So, delaying as much as possible its cluttering comes without any questions for me, every time.

Except.....

Notable exceptions

Life wouldn't be life if there were no "it depends". So there are times were creating extra endpoints is a better idea:

  1. Incoming webhooks. If a server pings your app when an event fires on their end: he will just ping one endpoint, period. So that endpoint better do everything that is needed to do. This same case can also be applied if your app offers an external API to other services.
  2. Security. There are times were, in order to make more than one action, we need to pass some sensitive data. And in those cases it's way better to duplicate parts of your back-end, than sending back that sensitive data to the client (where any funny person can do whathever he/she wants with it), to re-send it back to the server for the following API call.
  3. Ensuring the process finished. The more API calls you make: the more prone to errors, bugs, connectivity issues... You control what happens inside a function call of your server, much more than how much you control the millions of different devices and clients that hit it. If the operation that is being processed is really important: don't take your chances.

And you may be thinking that, if we are going to make more complex the back-end on those cases... Why not doing it for the rest as well?

Well I think these exceptions only prove me more right. The server is a precious resource: it's safer, usually more performing, and there is way less variability than in the client. So if you have any change to make it more maintainable, simpler, safer to errors... by off-setting some complexity to the client: do it. Because that way you will have more of those precious resources for the use-cases that can only go there.