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.
If you've built out a REST API in Node (or other languages, for that matter), you've likely used the concept of "controllers" to help organize your application. Maybe you put your calls to your database or models there, called some other endpoints, and added some business logic to process the returns.
That controller is something that probably looks like this:
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)
}
}
But what you might not have used as much, or even heard of, is the concept of "services". Or maybe you have heard of the concept and heard that you should be using them, but are questioning what logic goes there compared to what goes in your controllers.
Using services in API's is something I don't often see in Node-land, but is such a powerful addition to the structure of your API that will make it much easier for testing, code organization, and code re-use.
So if they are such a helpful way of structuring your API, what exactly are services?
In order to answer this question, we'll go into what the differences between controllers and services are and what goes where so you can more properly structure your Node API's.
A Manager / Worker analogy
One of the most helpful ways I can think of to explain the differences between the two is by using an analogy from the business world - the "manager" / "worker" dichotomy. We'll be using simplified stereotypes of what a manager does and what a worker does - I'm in no way saying that all managers have one type of role and workers have another!
In our analogy, the controller is the manager, while the service is the worker.
If you think about what the manager's role is, he/she typically:
- manages the incoming work requests
- decides which worker should do the work
- splits up the work into sizable units
- passes that work off
- if the work requires multiple people working on multiple things, orchestrates the work
- but does not do the work himself/herself (again, using a basic stereotype here!)
And, a worker typically:
- receives the request from the manager
- figures out the individual details involved in completing the request
- is generally only concerned with the tasks he/she has to complete
- not responsible for making decisions about the "bigger" picture
- does the actual work necessary to complete the tasks/request
- returns the completed work to the manager
The overarching theme here is that the manager/controller receives the work, decides who should do it, then passes off the request to be completed. While the worker/service is the one that takes that request and actually completes it. And you maybe have multiple workers working on different requests/tasks that complete the bigger picture, which the manager joins together so it makes sense.
What logic goes where?
Using this analogy, let's look at controllers vs. service from a technical perspective:
A controller:
- manages the incoming
workHTTP requests - decides
which workerwhat service should do the work - splits up the work into sizable units
- passes
that workthe necessary data from the HTTP requests off to the service(s) - if the work requires multiple
peopleservices working on multiple things, orchestratesthe workthose service calls - but does not do the work himself/herself
(again, using a basic stereotype here!)(not a stereotype here, the controller shouldn't be doing the work)
To sum up the above, the controller takes what it needs from Express (or whatever framework you're using), does some checking/validation to figure out to which service(s) should the data from the request be sent to, and orchestrates those service calls.
So there is some logic in the controller, but it is not the business logic/algorithms/database calls/etc that the services take care of. Again, the controller is a manager/supervisor.
And a service:
- receives the
requestdata it needs from the manager in order to perform its tasks - figures out the
individual detailsalgorithms/business logic/database calls/etc involved in completing the request - is generally only concerned with the tasks he/she has to complete
- not responsible for
making decisions about the "bigger" pictureorchestrating the different service calls - does the actual work necessary to complete the tasks/request
- returns
the completed worka response to the manager
Now summing up the service, the service is responsible for getting the work done and returning it to the controller. It contains the business logic that is necessary to actually meet the requirements and return what the consumer of the API is requesting.
A note on what is meant by "business logic"
I like to think of business logic as the more "pure" form of logic. It is logic that doesn't (usually!) care about validating the request or handling anything framework-specific. It just handles algorithms/rules for processing data, storing of data, fetching data, formatting that data, etc. These rules are usually determined by business requirements.
For example, if you had an API that returned how many users had been registered on your platform within the last X amount of days, the business logic here would be querying the database and doing any formatting of that data before it returned it to the controller.
Example of controller and service separation
Let's refactor our original controller-only code to look at an example of what this separation of concerns between controllers and services might look like:
First we'll pull out the logic for adding the user into a service.
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
}
Next we'll pull out the logic for sending a registration email to the user.
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
}
And finally, we'll greatly simplify the controller to simply make these two service calls:
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
}
In summary
That about wraps it up. Hopefully now you have a better understanding of what logic goes in a controller vs. what goes in the service. The easy way of remembering it is:
- controller: managers/orchestrates the work
- service: executes the work
Separating like this becomes a powerful tool for code reuse and code organization. Try it out with the next REST API you're building and I think you'll find it helps a lot.
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 (4)
@ccleary00 , have you tried NestJS? With its dependency injection your controller is totally unaware of whether the user or email service is an AWS service, in-memory service, stub service, etc. The controller just knows there will be an
addUser
method to the user service and what that service does under the hood, the controller doesn't know or care.Anything to help separate concerns (and reuse and testing!)
I haven't tried it out yet, but will play around with it soon (I keep seeing it mentioned lately)
Very well written!
Very good, thanks!