Introduction
Service Layers are a really useful abstraction that we create to give us the ability to ignore implementation details of our data layer. Essentially we create a series of methods on our Service Layer that will talk to our Data Layer (usually a database) and perform any operations we need for us. That way we can call the methods that exist in our Service Layer and change implementation details at any point leaving our application totally unaffected.
Implementation
The best way to understand exactly what a Service Layer does for us is to examine an application without one and then see how it changes with one. Let's start by looking at a controller that handles books in an application that doesn't have aservice layer and interacts with the db directly.
const db = require('../util/database')
module.exports = {
readAllBooks (req, res) {
db.execute(`SELECT * FROM books`)
.then(([books, fieldData]) => {
res.render('books/all', { books })
})
.catch(console.error)
},
readBook (req, res) {
db.execute(`SELECT * FROM books WHERE id=${req.params.id}`)
.then(([book, fieldData]) => {
res.render('books/single', { book: book[0] })
})
.catch(console.error)
}
}
Here is a controller in an application that doesn't have a service layer managing it's data requests. To be clear, this will work. This is a messy way to go about this though. This also leads to a tight coupling problem where our controller is "tightly coupled" with the database. If anything about our database changes, so too must our implementation in our controller. This is always undesirable. This is where a service layer comes in handy.
Service Layer Implementation
Let's take a look at what our service layer is actually going to look like.
const db = require('../util/database')
module.exports = {
findAll (callback) {
return db.execute('SELECT * FROM books')
.then(([books, fieldData]) => {
callback(books)
})
.catch(console.error)
},
findById (id, callback) {
return db.execute(`SELECT * FROM books WHERE id=${id}`)
.then(([books, fieldData]) => {
callback(books[0])
})
.catch(console.error)
}
}
Now with this service layer, we have two methods that handle getting all books in the db as well as getting one book by it's id
. Both of these methods accept a callback function that we pass to the then
block of the Promise. Now this module handles all of our interaction with the db. If we change what db we're using or how, that behavior only needs to change here in the Service Layer. We don't have to worry about what middleware, controllers, or any other piece of our application for that matter, will be affected by that shift.
Let's look at what our Controller looks like now.
const booksService = require('../services/books-service')
module.exports = {
readAllBooks (req, res) {
booksService.findAll((books) => {
res.render('books/all', { books })
})
},
readBook (req, res) {
booksService.findById(req.params.id, (book) => {
res.render('books/single', { book })
})
}
}
This is so much more simplified now. We have descriptive method names that tell us exactly what to expect from them. We also have a simplified syntax that we need to use in our controller by only having to create one simple anonymous function that handles the business of passing model data and rendering our view templates.
Conclusion
Using this sort of abstraction over our Data Layer helps to keep us organized as well as descriptive. It keeps us obeying the Single Responsibility Principle and writing clean code! Any time you can add abstractions over complex pieces of functionality, you're almost always going to benefit. Look for these opportunities every chance you get!