Layers Architecture
Introduction
Express is a popular Node.js web framework that provides developers with tools and features to create robust and scalable web applications. One of the key features of Express is its flexibility, which allows developers to structure their projects in a variety of ways. However, to create a maintainable and scalable application, it's important to use a layered architecture, which includes controller and service layers. In this doc, we'll explain why these layers are essential and how they work together to create a well-structured Express project.
What is a Layer?
In software engineering, a layer refers to a logical separation of concerns within a software system. Each layer typically provides a specific set of functionality or services, with well-defined interfaces for communication between layers.
Why use Layers?
Layers are often used to modularize software systems, which makes them easier to understand, maintain, and modify. Each layer has a specific role to play in the overall architecture of the system, and each layer communicates with the other layers through well-defined interfaces.
By separating concerns into layers, software systems become more organized and maintainable. Developers can work on individual layers without affecting other parts of the system, and changes made to one layer do not necessarily require changes to be made in other layers. Layers also enable code re-usability, as well as the ability to easily replace or upgrade individual layers without affecting the entire system.
Controller and Service Layers
Controller and service layers are two commonly used (kind of essential) components of a layered architecture. The controller layer is responsible for handling requests and responses between the client and server, while the service layer is responsible for business logic and data manipulation.
Let's take a look at how these two layers work together
Controller Layer
The controller layer serves as the entry point for all incoming requests. Its main function is to receive HTTP requests and return appropriate responses. Controllers in Express are responsible for validating request data, invoking services to execute business logic, doing database operations, and returning responses to the client. By using a controller layer, developers can isolate the HTTP-specific code from the rest of the application logic, which makes it easier to test and maintain.
Service Layer
The service layer is responsible for encapsulating business logic, data manipulation, and database operation. Services are also responsible for handling complex business logic, database operations, etc. By separating business logic from the controller layer, developers can improve the modularity and maintainability of their code.
How Do Controller and Service Layers Work Together?
In an Express project, the controller layer is responsible for handling incoming requests and delegating business logic to the service layer. Controllers in Express can be thought of as glue code that connects the HTTP layer to the service layer. Once a request is received, the controller layer validates the request data and then invokes the appropriate service to execute business logic. The service layer then returns the result to the controller layer, which formats the response and returns it to the client.
Check this diagram to understand the flow of data between the controller and service layers.
Let's Code
We will create a simple project and will use MongoDB with it. The project's structure will be like this:
📂 layer-architecture
├── 📂 controllers
│ └── 📄 book.js
├── 📂 models
│ └── 📄 book.js
├── 📂 routes
│ └── 📄 book.js
├── 📂 services
│ └── 📄 book.js
├── 📄 index.js
└── 📄 package.json
First of all, I am creating a folder named layer-architecture.
package.json
I will create a package.json file using the following command:
npm init -y
Now, I will install the required dependencies using the following command.
npm i express mongoose
After making some changes in the package.json file, the file will look like this:
{
"name": "layers",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
},
"author": "@mrizwanashiq",
"license": "ISC",
"dependencies": {
"express": "^4.17.2",
"mongoose": "^6.5.0",
"nodemon": "^2.0.15"
}
}
index.js
The index.js file will be the entry point of our application. I will create this file in the root directory of the project. I will import the required dependencies and will create an Express application. I will also connect the application with MongoDB.
import express from "express";
import mongoose from "mongoose";
import bookRouter from "./routes/book.js";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const connection = mongoose.connection;
connection.once("connected", () => console.log("Database Connected ~"));
connection.on("error", (error) => console.log("Database Error: ", error));
mongoose.connect("mongodb://127.0.0.1:27017/my_first_data_base");
app.use("/book", bookRouter);
app.listen(3000, () => console.log("Server Started ~"));
I will add the code related to routes, controllers, models, and services in the respective folders, and will import the routes in the index.js file, and attach them to the Express application.
Let's create a book model in the models folder.
models/book.js
import mongoose from "mongoose";
const schema = mongoose.Schema({
name: { type: String, required: true },
author: { type: String, required: true },
price: { type: Number, required: true },
stock: { type: Number },
});
export default mongoose.model("Book", schema);
Will use this model in the book service. Now, let's create a book service in the services folder.
services/book.js
import book from "../models/book.js";
const BookService = {
get: async (query) => {
return book.find(query);
},
getById: async (id) => {
return book.findById(id);
},
create: async (data) => {
return book.create(data);
},
update: async ({ id, ...rest }) => {
return book.findByIdAndUpdate(id, rest);
},
delete: async (id) => {
return book.findByIdAndDelete(id);
},
};
export default BookService;
This service will be used in the book controller. Now, let's create a book controller in the controllers folder.
controllers/book.js
import BookService from "../services/book.js";
const BookController = {
get: async (req, res) => {
try {
const books = await BookService.get(req.query);
res.json(books);
} catch (err) {
res.json({ message: err.message });
}
},
getById: async (req, res) => {
try {
const book = await BookService.getById(req.params.id);
res.json(book);
} catch (err) {
res.json({ message: err.message });
}
},
create: async (req, res) => {
try {
const result = await BookService.create(req.body);
res.status(200).json(result);
} catch (err) {
res.json({ message: err.message });
}
},
update: async (req, res) => {
try {
const result = await BookService.update(req.params.id, req.body);
res.status(200).json(result);
} catch (err) {
res.json({ message: err.message });
}
},
delete: async (req, res) => {
try {
const result = await BookService.delete(req.params.id);
res.status(200).json(result);
} catch (err) {
res.json({ message: err.message });
}
},
};
export default BookController;
You can see, I called the relevant service methods in the controller methods. Now, let's create a book route in the routes folder.
routes/book.js
import express from "express";
import BookController from "../controllers/book.js";
const router = express.Router();
router.get("/", BookController.get);
router.get("/:id", BookController.getById);
router.post("/", BookController.create);
router.patch("/:id", BookController.update);
router.delete("/:id", BookController.delete);
export default router;
Now, I will import this route in the index.js file and will attach it to the Express application.
import express from "express";
import mongoose from "mongoose";
import bookRouter from "./routes/book.js";
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
const connection = mongoose.connection;
connection.once("connected", () => console.log("Database Connected ~"));
connection.on("error", (error) => console.log("Database Error: ", error));
mongoose.connect("mongodb://127.0.0.1:27017/my_first_data_base");
app.use("/book", bookRouter);
app.listen(3000, () => console.log("Server Started ~"));
Now, I will run the server and will test the API using Postman.
npm start
Code
You can find the code for this doc from here