Originally published at coreycleary.me. This is a cross-post from my content blog. I publish new content every week or two, and you can sign up to my newsletter if you'd like to receive my articles directly to your inbox! I also regularly send cheatsheets and other freebies.
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.
Love JavaScript but still getting tripped up by architecture stuff and how you should structure your service? I publish articles on JavaScript and Node every 1-2 weeks, so if you want to receive all new articles directly to your inbox, here's that link again to subscribe to my newsletter!
Top comments (3)
Hi, thanks for this post.
Do you have any recommendation on where to start learning about “web logic”, “business logic”, middleware, controllers, and services? I’m generally at intermediate skill level but I’m terrible at this kind of thing.
Yeah, different types of logic, and where that logic goes in your project is a tricky thing to figure out. Especially in the Node.js world, where there aren't a lot of conventions. I cover most of that in another post I wrote. It covers what the different types of logic are, where you should put them in your project (i.e. - controllers vs services), and how you should structure your project.
The main advantage of having the business code separate from the controller is you can automate tests easily, assuming you really separated the business code from the controller and no "web server" or "web request" or "request context" etc. objects leak into the business code.
Think about your very simple "search" example: you want to write a test for the controller, you have to mock the server somehow and that can get painful, intricate, expensive and sometimes even unreliable.
If you can run the code in the "service" as instantiating some plain data objects and passing then to a function you can write tests easily and fast.
You might be mistaking complexity for size: writing services means more code but if all those services are not coupled and exchange only basic data structures or plain objects it will be a lot simpler and easier to work with them. Think about using Legos versus building something with custom pieces of plastic glued together.