DEV Community

Cover image for Why should you separate Controllers from Services in Node REST API's?
Corey Cleary
Corey Cleary

Posted on • Originally published at coreycleary.me

Why should you separate Controllers from Services in Node REST API's?

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.

This is a follow-up to my last post, What is the difference between Controllers and Services in Node REST API's?. In that post we covered the differences between the two, and what kind of logic goes where, but only briefly touched on why you might want to do this.

You might still be wondering, "why is it a good idea to separate the two?". Why use services when controllers are already working?

The WHY is what we'll be going into in more depth in this post.

Using controllers only

If you have a really small application, like only a couple simple routes and controllers, and haven't pulled out your business logic into any services, you probably haven't gotten too annoyed by your current structure yet. And to be clear, we're talking about service files within a project, not separate REST services.

But if your application has grown beyond that, I bet you've experienced several of the following pains:

  • Controllers that have lots of code in them, 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.
  • You have no idea where to even begin writing 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.

How does separation help?

To re-iterate from the previous post on this subject, what you're exactly separating from controllers and services is the business logic from the web/HTTP logic.

So your controllers handle some basic things like validation, pulling out what data is needed form the HTTP request (if you're using Express, that's the req object) and deciding what service that data should go to. And of course ultimately returning a response.

While the services take care of the heavy lifting like calling the database, processing and formatting data, handling algorithms based on business rules, etc. Things not specific to the HTTP layer, but specific to your own business domain.

After doing this separation, those pains mentioned above greatly lessen, if not go away entirely. That's the beauty of using services. Yes there will always be refactoring and things that are difficult to test, but putting things into services makes this much easier.

And this is the WHY.

Let's go over each of these pains one by one. Below is a code example where all the logic is in the controller, from the previous post:

const registerUser = async (req, res, next) => {
  const {userName, userEmail} = req.body
  try {
    // add user to database
    const client = new Client(getConnection())
    await client.connect()

    await client.query(`INSERT INTO users (userName) VALUES ('${userName}');`)
    await client.end()

    // send registration confirmation email to user
    const ses = new aws.SES()

    const params = { 
      Source: sender, 
      Destination: { 
        ToAddresses: [
          `${userEmail}` 
        ],
      },
      Message: {
      Subject: {
        Data: subject,
        Charset: charset
      },
      Body: {
        Text: {
          Data: body_text,
          Charset: charset 
        },
        Html: {
          Data: body_html,
          Charset: charset
        }
      }
    }

    await ses.sendEmail(params) 

    res.sendStatus(201)
    next()
  } catch(e) {
    console.log(e.message)
    res.sendStatus(500) && next(error)
  }
}

Controller with lots of code, bloated and cluttered - AKA "fat controller"

You may have heard the term "fat controller" before. It's when your controller has so much code in it that it looks, well, fat.

This obviously makes it more difficult to read and figure out what the code is doing. Having long and complex code is sometimes unavoidable, but we want that code to be isolated and responsible for one general thing.

And because the controller should orchestrate several different things, if you don't have those different things pulled out into services they'll all end up in the controller, growing the amount of code contained there.

By pulling out the business logic into services, the controller becomes very easy to read. Let's look at the refactored version of the above code using services:

Simplified controller:

const {addUser} = require('./registration-service')
const {sendEmail} = require('./email-service')

const registerUser = async (req, res, next) => {
  const {userName, userEmail} = req.body
  try {
    // add user to database
    await addUser(userName)

    // send registration confirmation email to user
    await sendEmail(userEmail)

    res.sendStatus(201)
    next()
  } catch(e) {
    console.log(e.message)
    res.sendStatus(500) && next(error)
  }
}

module.exports = {
  registerUser
}

Registration service:

const addUser = async (userName) => {
  const client = new Client(getConnection())
  await client.connect()

  await client.query(`INSERT INTO users (userName) VALUES ('${userName}');`)
  await client.end()
}

module.exports = {
  addUser
}

Email service:

const ses = new aws.SES()

const sendEmail = async (userEmail) => {
  const params = { 
    Source: sender, 
    Destination: { 
      ToAddresses: [
        `${userEmail}`
      ],
    },
    Message: {
      Subject: {
        Data: subject,
        Charset: charset
      },
      Body: {
        Text: {
          Data: body_text,
          Charset: charset 
        },
        Html: {
          Data: body_html,
          Charset: charset
        }
      }
    }
  }

  await ses.sendEmail(params) 
}

module.exports = {
  sendEmail
}

Now we have a "thin controller" and can much more easily figure out what's going on.

Can't reuse code

Another big problem is that you can't reuse your code. Let's say we wanted to use the same email-sending code in another controller somewhere else, maybe one supporting an API route that sends emails for followup comments on a Reddit-style forum.

We'd have to copy that code and make some adjustments, rather than just making an email service that is generalized enough to send different kinds of emails, and importing that service into each controller that needs it.

Difficult to refactor

Following on the above two problems, when we don't have business logic isolated to services, it becomes more difficult to refactor and/or add new features.

If code is cluttered and bloated, it's much more difficult to refactor without accidentally breaking some other code in proximity. That's the more obvious one.

But what if we have to add a new feature or new functionality? Imagine if we now had two controllers that both sent emails out after some event was triggered (user registered, user received a follow-up comment on their post, etc). If we had two separate pieces of very similar email code, and we wanted to change the email provider (say from AWS to Sendgrid). We'd have to make that change in two places now! And change the tests in two places as well.

Difficult to write tests

Lastly, and this is a big one, when you don't make use of services it becomes much more difficult to write tests for the logic you're trying to cover.

When you have controllers with multiple different pieces of logic in them, you have multiple code paths you have to cover. I wouldn't even know where to start with writing a test for the controller-only example above. Because it is doing multiple things, we can't test each of those things in isolation.

But when code is more isolated, it becomes easier to test.

And with services, there is no HTTP request object or web framework we have to deal with. So our tests don't have to take that into consideration. We don't have to mock the req and/or res objects.

Once the business logic is pulled out into services, and you have tests written for those, I'd argue you might not even need tests for the controller itself. If there is logic that decides which service to route the request to, then you might want tests for that. But you can even test that by writing some end-to-end tests using supertest and just calling the API route to make sure you get the correct responses back.

Wrapping up

So should you start with controllers then pull business logic out into services later? Or should you start with them from the beginning? My recommendation is to start each project / new feature where you need to add a controller by separating it into a controller and services. It's what I do with every application I work on.

If you already have an application that is not making use of services, for each new feature you need to add, if it's a new route/controller, start with the services approach. And if it doesn't require a new controller, try to refactor the existing one into using services.

You'll make it much easier on yourself in the long run, for all of the reasons discussed above, plus you'll get used to practicing structuring projects in this way.

I'm writing a lot of new content to help make Node and JavaScript easier to understand. Easier, because I don't think it needs to be as complex as it is sometimes. If you enjoyed this post and found it helpful here's that link again to subscribe to my newsletter!

Top comments (10)

Collapse
 
anduser96 profile image
Andrei Gatej

I just started a project in which I use Express and my app structure also implies the use of controllers and services.

However, I decided to approach this app structure in an OO way.

class Controller {
    constructor (name) {
        const Service = require('../services');
        this.service = new Service(name);
    }

    insertOne (req, res) {
        const paramsBody = req.params.body;

        this.service.insertOne(paramsBody);

        res.json({
            message: 'success'
        })
    }
}

Here’s the Service class.


class Service {
    constructor (name) {
        const dbName = require('../db')[`${name}DB`];
        this.db = new dbName();
    }

    inserOne (params) {
        // business logic here 
        return this.db.insertOne(params);
    }
}

My question is: do you think using OOP in this case is redundant? I mean, this could be achieved with plain js objects.
If you were to work on a project with a structure like this, would you feel comfortable working on further?

Thank you for your time!

Collapse
 
ccleary00 profile image
Corey Cleary

Hey, great question Andrei - I personally tend to favor using plain objects and functions over using classes. In general, I try to follow the mantra of "don't use a class where a function will work just as well". I've seen the bad things that can happen when classes get inherited too much and creates complex understanding of what 'this' refers to, having to trace back up the class hierarchy, etc.

That being said, I think there are two things to consider: 1) if you're working on a team that is more familiar with the class-based approach, then it's probably worth using that approach (although it's definitely worth discussing with your team if classes are really needed are not). 2) sometimes Services need setup in the constructor and/or to maintain state. That is a more valid use case for a class (although you could look into using a factory function instead if you have some complex setup).

Either way - classes/OOP vs just functions - the structure/code organization is what I think is most important. And not putting business logic in the controllers.

Collapse
 
anduser96 profile image
Andrei Gatej

Thank you for your answer!

Initially I was going for plain objects, but then I realized that I would repeat the same functions for almost each service(same goes for controllers).

So I thought I would use a single blueprint service class and in the constructor I would choose the right db class using a factory pattern(that’s at least how I see things, it seems to be working well).

Anyway, I make sure that business logic goes into services, and the controller is the ‘orchestrator’.

Best of luck!

Collapse
 
mazyvan profile image
Iván Sánchez

Have you guys ever tried nest.js? It is a really really cool node.js framework that solved many of the architectural design problems on node applications. It's very SOLID friendly and uses all of this great kind of stuff (Controllers, Services, Entities, DTOs, Repositories, and many more) also it uses typescript by default and is build on top of express.js

I'm thinking of write a post about it. So follow me to check it out later.

Collapse
 
anduser96 profile image
Andrei Gatej

Sounds awesome! Can’t wait for the post!

Collapse
 
lschultebraucks profile image
Lasse Schultebraucks

This of course does not only apply to Node Rest API, this applies to every backend Rest Service, no matter of the technology used.

Also mapping and the separation of DTOs and Enties is very important.

Collapse
 
janguianof profile image
Jaime Anguiano

very useful : ) please more posts like this!

Collapse
 
ccleary00 profile image
Corey Cleary • Edited

Glad to hear it, and you got it

Collapse
 
klanmiko profile image
Kaelan Mikowicz

I like the idea of refactoring controllers into smaller services. Though, how does the concept of a service differ from a regular async function?

Collapse
 
ccleary00 profile image
Corey Cleary • Edited

Services are just an organizational concept - and they don't have to be async functions, they can be any function (or collection of functions).