Do you always need to separate services and controllers?

You might have heard that you should separate your web logic (HTTP routes, middleware, and controllers) from your business logic (services). I've written about this before, and I highly recommend giving it a read if you haven't yet, as it will form the basis of the rest of this post.

But as a recap, you want to pull out business logic code into services because:

  • Controllers can end up doing lots of things - AKA "fat controllers".
  • Closely related to the previous one, your code looks cluttered. With controllers making 4 or 5 or more database/model calls, handling the errors that could come with that, etc., that code probably looks pretty ugly.
  • All your logic in your controllers makes it really difficult to writes tests.
  • Requirements change, or you need to add a new feature and it becomes really difficult to refactor.
  • Code re-use becomes pretty much non-existent.

But what if your route isn't doing that much? What if all it needs to do is fetch an item from the database and return it?

Something like this:

// route
router.post('/search', itemController.search)
// item controller
const search = async (req, res, next) => {
  const { term } = req.body
  const items = await itemService.searchItems(term)
  res.send(items)
}
// item service
const searchItems = async (term) => {
  return itemQuery.search(term)
}
// item database query
const search = async (term) => {
  return db.select('*').from('item').where('name', 'like', '%${term}%')
}

Do you really need to create that service if all it's doing is calling the database and nothing else? Or can you just put that database code in the controller itself?

Let's dive into the pros and cons so you are better equipped to make a choice.

Approach 1 - Going with a service no matter what

Even if all you need to do is make a database call and return that result from your route, let's imagine you put that database call logic in a separate service and not just a controller. What would the effects of this be?

PROS:

  • (Avoid all the issues described above)
  • You get "thin" controllers, right from the start
  • Can write tests much more easily, right from the start
  • Easier to refactor when requirements change... your code is already separated into a service rather than it all bunching up in the controller, right from the start

These are all pretty big advantages, but let's look at a disadvantage I see with this approach.

CONS:

  • You have an extra file (item.service.js), which results in more wiring up (importing/exporting) you have to do

Now, in my opinion, this is not that big a deal... the advantages far outweigh this minor inconvenience, and as your app grows with code and features, you're likely going to have to pull the business logic out into a service if you haven't already done so anyways.

Approach 2 - Skipping the service, just putting business logic in controller

Now let's take a look at the advantages and disadvantages of the opposite approach.

PROS:

  • There is less wiring up you have to do - you can put all the code in your controller and not have to add separate service and/or database/models files.
  • If the route is simple, it can be easier to see all your logic in one file.

CONS:

  • Off the bat, you pretty much have to test your app only through the route, using something like supertest.
  • You can't unit test your code, with all that logic in one place, right now it's all an integration test.
  • When the app gets more complex, future refactoring has the potential to more difficult. The more logic you need to pull out and isolate into a service, the more potential for breaking things and introducing new bugs.

Let's imagine that the search controller we described at the beginning of this post now needs to do check against a separate service for how those search results should be ranked and check a different service for promotional deals on those items we are returning from the search. It just got much more complex, and shoving all that logic in the controller is going to get messy. Quickly.

Conclusion

If you can live with the extra wiring up work, my recommendation is to include the service, even if it's simple. If you've worked in software development for even a short amount of time, you know how quickly and how often requirements can be changed, added, or removed. With those requirements changes comes changes to the business logic, which means the controller will get more complex and you're going to have to pull that logic out into a service anyways. So might as well start off with having the service.

If this is a small side project and you're writing throwaway code, or if you're at a hackathon and working against the clock to ship something fast, putting that business logic / database access code in the controller is probably fine. But if this is going to be a project that will be in production and worked on by several developers, start off with the service.

As always, each app is unique and has unique requirements and design. But use the above comparison the next time you are faced with making this decision and it will help guide you in your design choices.

Found this post helpful? One of the most frustrating things with Node is how there aren't many "official" patterns for ways to do things, like how to structure your REST API's for example. Figuring out the difference between controllers and services is one part of figuring out how to structure your app, but it's only one part of the puzzle. If you want the rest of the picture, sign up below to receive a template repo with the structure I use for all my Express REST API's and a post explaining in detail what logic goes where within that structure. You'll also receive all my future posts directly to your inbox!

Subscribe for the repo!

No spam ever. Unsubscribe any time.