(Code) GitHub
- The most widely applied pattern for backend API code
- a.k.a Multi-tier architecture pattern
- Separate the cord into an independent module
- One-way layer dependency, separation of concerns
- Why layerd pattern? Scalability, Reusability, Maintenance, Readability, Testability
- 3 Layer : Presentation, Business, Persistence
- Flow : Controller → Service → Model
0. App
app.js (server.js)
- Start Express app Server
- Create Express app instance and add middleware
- You can create app.js and server.js. Or create only one file.
const http = require("http");
const express = require('express');
const cors = require('cors');
const morgan = require('morgan');
const dotenv = require("dotenv")
dotenv.config()
const routes = require("./routes");
const app = express();
app.use(cors());
app.use(morgan('combined'));
app.use(express.json());
app.use(routes);
app.get("/ping", (req, res) => {
res.json({ message: "pong" });
});
const server = http.createServer(app);
const PORT = process.env.PORT;
const start = async () => {
try {
server.listen(PORT, () => console.log(`Server is listening on ${PORT}`));
} catch (err) {
console.error(err);
}
};
start();
1. Presentation Layer (Endpoint)
- Connect directly user or client system
- Endpoint of Backend API (Define endpoints of API)
- Accept HTTP request
Routes
- Routing endpoint directory
index.js
- Manage various router(ex: userRouter, productRouter)
- Routing(Seperating endpoint) logic
- Receive requests from outside and direct them to subfolders (=Router role)
const express = require("express");
const router = express.Router();
const userRouter = require("./userRouter");
router.use("/users", userRouter.router);
module.exports = router;
userRouter.js
const express = require("express");
const userController = require("../controllers/userController");
const router = express.Router();
router.post("/signup", userController.signUp);
module.exports = {
router,
};
(Additional) postRouter.js
- Verify (JWT) token on router.
const express = require("express");
const postController = require("../controllers/postController");
const router = express.Router();
const { validateToken } = require("../middlewares/auth.js");
router.post("/", validateToken, postController.createPost);
module.exports = {
router,
};
Controller (Presentation Layer)
userController.js
- Treat request, response
- Define endpoint
- Error handleing by http request
- Response data to outside from business(service) layer
- Check the data before send it to business(service) layer (=Error check)
- ex) Have I received all the data from the FrondEnd?
const userService = require("../services/userService");
const signUp = async (req, res) => {
try {
const { name, email, password, profileImage } = req.body;
if (!name || !email || !password || !profileImage) {
return res.status(400).json({ message: "KEY_ERROR" });
}
await userService.signUp(name, email, password, profileImage);
return res.status(201).json({
message: "SIGNUP_SUCCESS",
});
} catch (err) {
console.log(err);
return res.status(err.statusCode || 500).json({ message: err.message });
}
};
module.exports = {
signUp,
};
2. Business Layer(Service Layer) (Verification)
- Business(Service) logic
- Logic required to run the company's business
- ex) Verify password
Service
- Controller send parameter to Service layer
- In service layer, process it by various algorithms (filters, sorting...)
- And send it / connect to DB
- Business rules and logic should be applied.
userService.js
- Verifying Password Rule
- Compare password hash and create JWT token
require("dotenv").config();
const bcrypt = require("bcrypt");
const jwt = require("jsonwebtoken");
const userDao = require("../models/userDao");
const saltRounds = 12;
const secretKey = process.env.SECRET_KEY;
const signUp = async (name, email, password, profileImage) => {
const pwValidation = new RegExp(
"^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20})"
);
if (!pwValidation.test(password)) {
const err = new Error("PASSWORD_IS_NOT_VALID");
err.statusCode = 409;
throw err;
}
const hashedPassword = await bcrypt.hash(password, saltRounds);
const createUser = await userDao.createUser(
name,
email,
profileImage,
hashedPassword
);
return createUser;
};
const login = async (email, password) => {
const pwValidation = new RegExp(
"^(?=.*[A-Za-z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,20})"
);
if (!pwValidation.test(password)) {
const err = new Error("PASSWORD_IS_NOT_VALID");
err.statusCode = 409;
throw err;
}
const hashedPassword = await userDao.getHashedPassword(email);
console.log(hashedPassword);
if (!(await bcrypt.compare(password, hashedPassword))) {
const err = new Error("PASSWORD_IS_NOT_VALID");
err.statusCode = 401;
throw err;
}
const userId = await userDao.getUserID(email);
const payLoad = { userId: userId };
const jwtToken = jwt.sign(payLoad, secretKey);
return jwtToken;
};
module.exports = {
signUp,
login,
};
3. Persistence Layer (DB)
- Get parameter from Business layer
- Save/Modify/Read data from DB
Models
myDataSource.js
const { DataSource } = require("typeorm");
const myDataSource = new DataSource({
type: process.env.TYPEORM_CONNECTION,
host: process.env.TYPEORM_HOST,
port: process.env.TYPEORM_PORT,
username: process.env.TYPEORM_USERNAME,
password: process.env.TYPEORM_PASSWORD,
database: process.env.TYPEORM_DATABASE,
});
myDataSource
.initialize()
.then(() => {
console.log("Data Source has been initialized!");
})
.catch((err) => {
console.error("Error occurred during Data Source initialization", err);
myDataSource.destroy();
});
module.exports = {
myDataSource,
};
userDao.js (Data Access Objective)
- Define DAO (data access object) to access DB
const { myDataSource } = require("./myDataSource");
const createUser = async (name, email, password, profileImage) => {
try {
return await myDataSource.query(
`INSERT INTO users(
name,
email,
profileImageUrl,
password
) VALUES (?, ?, ?, ?);
`,
[name, email, password, profileImage]
);
} catch (err) {
console.log(err);
const error = new Error("INVALID_DATA_INPUT");
error.statusCode = 500;
throw error;
}
};
module.exports = {
createUser,
};
99. etc
- Available in a variety of layers without dependency
- Repeated logic
- middlewares: Folders that modularize repeated logic before touching the controller (ex. validateToken - Authentication / Authorization)
- utils: Folders that modularize commonly used logic across all layers without dependency (ex. errorGenerator)
- .env: Declare environment variables
- .env.sample: sample version of env
- node_modules: Node package module
- .gitignore: Ignore files/directories when we use git
- package.json: Manage node modules