Layered Pattern (with Express)

Judy·2023년 1월 1일
0

Dev

목록 보기
4/7

(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

  • Initialize 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();

// Module for verify JWT token
const { validateToken } = require("../middlewares/auth.js");

// Create post
// Verify JWT token
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; // (3)

// Sign up
const signUp = async (name, email, password, profileImage) => {
  // password validation using REGEX
  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;
  }

  // bcrypt
  const hashedPassword = await bcrypt.hash(password, saltRounds);

  const createUser = await userDao.createUser(
    name,
    email,
    profileImage,
    hashedPassword
  );

  return createUser;
};

// Additional example : Login
const login = async (email, password) => {
  // password validation using REGEX
  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;
  }

  // 1. Get hashed password from DB
  const hashedPassword = await userDao.getHashedPassword(email);
  console.log(hashedPassword);

  // 2. If input password != hashed password, Error!
  if (!(await bcrypt.compare(password, hashedPassword))) {
    const err = new Error("PASSWORD_IS_NOT_VALID");
    err.statusCode = 401;
    throw err;
  }

  // 3. Get user ID from DB
  const userId = await userDao.getUserID(email);

  // 4. Create JWT token & return
  const payLoad = { userId: userId };
  const jwtToken = jwt.sign(payLoad, secretKey); // (4)

  return jwtToken;
};

module.exports = {
  signUp,
  login,
};

3. Persistence Layer (DB)

  • Get parameter from Business layer
  • Save/Modify/Read data from DB

Models

myDataSource.js

  • Connect to DB by typeorm
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,
});

// connect DB
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

profile
NLP Researcher

0개의 댓글