Chia sẻ
//Xây dựng REST API quản lý users với Node.js/TypeScript (Phần 2: Controllers, Middleware, Services và Models)

Xây dựng REST API quản lý users với Node.js/TypeScript (Phần 2: Controllers, Middleware, Services và Models)

phần 1, chúng ta đã hoàn thành được phần khung của ứng dụng, tiếp theo chúng ta sẽ xây dựng những thành phần quan trọng khác:

  • Services: đây là tầng đóng gói các logic nghiệp vụ, giúp cho code trở nên clean hơn.
  • Middleware: sẽ xác thực các điều kiện tiên quyết trước khi Express.js gọi vào controller chỉ định.
  • Controllers: gọi vào tầng services để xử lý nghiệp vụ logic sau đó sẽ gửi response về client.
  • Models: tầng mô tả dữ liệu.

I. Bắt đầu với DAOs, DTOs và database tạm

Đối với phần này, chúng ta sẽ chưa kết nối với database, thay vào đó ta sẽ lưu dữ liệu và mảng và chỉ implement các phương thức cơ bản như create, read, update và delete.

Tiếp theo, ta sẽ xây dựng 2 đối tượng dưới đây:

  • Data Access Objects (DAOs): chịu trách nhiệm kết nối, định nghĩa database và thực hiện các thao tác CRUD.
  • Data Transfer Objects (DTOs): là một object giữ raw data mà DAO sẽ gửi và nhận từ database.

Đầu tiên tạo 1 folder tên “dto” bên trong folder users, và tạo 1 file tên post.user.dto.ts chứa nội dung sau:

export interface PostUserDto {
   id: string;
   email: string;
   password: string;
   firstName?: string;
   lastName?: string;
   permissionLevel?: number;
}

DTO post dùng để trao đổi dữ liệu khi tạo mới user.

Đối với update user, ta tạo DTO put.user.dto.ts có nội dung sau:

export interface PutUserDto {
   id: string;
   email: string;
   password?: string;
   firstName?: string;
   lastName?: string;
   permissionLevel?: number;
}

Đối với path, chúng ta có thể dùng chức năng Partial của Typescript để tạo ra kiểu mới bằng cách copy lại kiểu của put và biến tất cả các field thành option.

import { PutUserDto } from’./put.user.dto’;

import { PutUserDto } from './put.user.dto';

export interface PatchUserDto extends Partial<PutUserDto> {}

Để tạo ra user id, ta sẽ sử dụng thư viện shortid.

npm i shortid
npm i –save-dev @types/shortid

Kế tiếp, chúng ta sẽ tạo ra một database tạm để lưu trữ dữ liệu. Tạo 1 thư mục tên “daos” trong folder users, bên trong daos tạo 1 file tên users.dao.ts.

import { PostUserDto } from '../dtos/post.user.dto';
import { PatchUserDto } from '../dtos/patch.user.dto';
import { PutUserDto } from '../dtos/put.user.dto';

import shortid from 'shortid';

class UsersDao {
    users: Array<PutUserDto> = [];

    async addUser(user: PostUserDto) {
        user.id = shortid.generate();
        this.users.push(user);
        return user.id;
    }

    async getUsers() {
        return this.users;
    }
   
    async getUserById(userId: string) {
        return this.users.find((user: { id: string }) => user.id === userId);
    }

    async putUserById(userId: string, user: PutUserDto) {
        const objIndex = this.users.findIndex(
            (obj: { id: string }) => obj.id === userId
        );
        this.users.splice(objIndex, 1, user);
        return `${user.id} updated via put`;
    }
   
    async patchUserById(userId: string, user: PatchUserDto) {
        const objIndex = this.users.findIndex(
            (obj: { id: string }) => obj.id === userId
        );
        let currentUser = this.users[objIndex];
        const allowedPatchFields = [
            'password',
            'firstName',
            'lastName',
            'permissionLevel',
        ];
        for (let field of allowedPatchFields) {
            if (field in user) {
                // @ts-ignore
                currentUser[field] = user[field];
            }
        }
        this.users.splice(objIndex, 1, currentUser);
        return `${user.id} patched`;
    }

    async removeUserById(userId: string) {
        const objIndex = this.users.findIndex(
            (obj: { id: string }) => obj.id === userId
        );
        this.users.splice(objIndex, 1);
        return `${userId} removed`;
    }
}

export default new UsersDao();

Trong đoạn code phía trên ta đã tạo ra 1 class để chứa dữ liệu tạm trong RAM, bao gồm các phương thức để thao tác CRUD. Ở phần sau, ta sẽ kết nối với database để thao tác dữ liệu thay cho mảng dữ liệu tạm.

II. Services

Tạo một folder services trong thư mục users, tạo file users.service.ts có nội dung sau:

import UsersDao from '../daos/users.dao';
import { PostUserDto } from '../dtos/post.user.dto';
import { PutUserDto } from '../dtos/put.user.dto';
import { PatchUserDto } from '../dtos/patch.user.dto';

class UsersService {
    async create(resource: PostUserDto) {
        return UsersDao.addUser(resource);
    }

    async deleteById(id: string) {
        return UsersDao.removeUserById(id);
    }

    async list(limit: number, page: number) {
        return UsersDao.getUsers();
    }

    async patchById(id: string, resource: PatchUserDto) {
        return UsersDao.patchUserById(id, resource);
    }

    async readById(id: string) {
        return UsersDao.getUserById(id);
    }

    async putById(id: string, resource: PutUserDto) {
        return UsersDao.putUserById(id, resource);
    }
}

export default new UsersService();

Theo ví dụ, các service chỉ đơn giản gọi xuống DAO, nhưng trong thực tế, các nghiệp vụ logic sẽ được xử lý ở đây.

III. Controllers

Để xử lý hashing password, ta sử dụng thư viện argon2:

npm i argon2

Tiếp theo tạo thư mục controllers trong thư mục users và tạo file users.controller.ts bên trong:

import express from 'express';
import usersService from '../services/users.service';

// we import the argon2 library for password hashing
import argon2 from 'argon2';

class UsersController {
    async listUsers(req: express.Request, res: express.Response) {
        const users = await usersService.list(100, 0);
        res.status(200).send(users);
    }

    async getUserById(req: express.Request, res: express.Response) {
        const user = await usersService.readById(req.body.id);
        res.status(200).send(user);
    }

    async createUser(req: express.Request, res: express.Response) {
        req.body.password = await argon2.hash(req.body.password);
        const userId = await usersService.create(req.body);
        res.status(201).send({ id: userId });
    }

    async patch(req: express.Request, res: express.Response) {
        if (req.body.password) {
            req.body.password = await argon2.hash(req.body.password);
        }
        res.status(204).send();
    }

    async put(req: express.Request, res: express.Response) {
        req.body.password = await argon2.hash(req.body.password);
        res.status(204).send();
    }

    async removeUser(req: express.Request, res: express.Response) {
        res.status(204).send();
    }
}

export default new UsersController();

IV. Middleware

Tiếp theo ta sẽ tạo user middleware để validate request một số trường hợp sau:

  • Đảm bảo các field như email và password required cho trường hợp tạo/update user.
  • Đảm bảo user tồn tại.

Tạo thư mục middleware trong thư mục users, tạo file users.middleware.ts bên trong:

import express from 'express';
import userService from '../services/users.service';

class UsersMiddleware {
    async validateRequiredUserBodyFields(
        req: express.Request,
        res: express.Response,
        next: express.NextFunction
    ) {
        if (req.body && req.body.email && req.body.password) {
            next();
        } else {
            res.status(400).send({
                error: `Missing required fields email and password`,
            });
        }
    }
   
    async validateUserExists(
        req: express.Request,
        res: express.Response,
        next: express.NextFunction
    ) {
        const user = await userService.readById(req.params.userId);
        if (user) {
            next();
        } else {
            res.status(404).send({
                error: `User ${req.params.userId} not found`,
            });
        }
    }
}

export default new UsersMiddleware();

Ở đoạn code trên, req là request do client gọi lên, res là response sẽ trả về cho client, hàm next() sẽ đẩy request đi tới tầng tiếp theo của ứng dụng.

V. Chỉnh sửa lại routes

Mọi thứ đã sẵn sàng, giờ ta sẽ đặt mọi thứ lại cùng nhau và chỉnh sửa lại file users.routes.config.ts như sau:

import UsersController from './controllers/users.controller';
import UsersMiddleware from './middleware/users.middleware';
import express from 'express';

export class UsersRoutes {
    app: express.Application


    constructor(app: express.Application) {
        this.app = app;
        this.configureRoutes();
    }

    configureRoutes() {
        this.app
            .route(`/users`)
            .get(UsersController.listUsers)
            .post(
                UsersMiddleware.validateRequiredUserBodyFields,
                UsersController.createUser
            );

        this.app
            .route(`/users/:userId`)
            .all(UsersMiddleware.validateUserExists)
            .get(UsersController.getUserById)
            .delete(UsersController.removeUser);

        this.app.put(`/users/:userId`, [
            UsersMiddleware.validateRequiredUserBodyFields,
            UsersController.put,
        ]);

        this.app.patch(`/users/:userId`, [
            UsersController.patch,
        ]);

        return this.app;
    }
}

Ở đây, chúng ta đã định nghĩa lại route bằng cách thêm middleware để validate logic và đưa request đến controller thích hợp nếu mọi thứ hợp lệ.

Chúng ta cũng đã tận dụng khả năng tái sử dụng của middleware bằng cách sử dụng UsersMiddleware.validateRequiredUserBodyFields ở cả POST và PUT method.

VI. Test API

Tiến hành khởi chạy ứng dụng bằng câu lệnh:

npm start

Để test các api hoạt động, ta có thể sử dụng curl (trên linux) hoặc Invoke-WebRequest (trên windows):

Lấy danh sách users:

curl –request GET ‘localhost:3000/users’ \

–header ‘Content-Type: application/json’

Tạo mới user:

curl –request POST ‘localhost:3000/users’ \

–header ‘Content-Type: application/json’

Nếu không có đủ thông tin email và password sẽ báo lỗi:

{

“error”: “Missing required fields email and password”

}

Để tạo mới user thành công, cần thêm thông tin email và password:

curl –request POST ‘localhost:3000/users’ \

–header ‘Content-Type: application/json’ \

–data-raw ‘{

    “email”: “[email protected]”,

    “password”: “123123123”

}’

Tương tự như trên để test các api còn lại.

VII. Lời kết

Thông qua 2 phần của bài viết về cách thức xây dựng REST API cũng như quản lý user với Node.js/Typescript, hy vọng sẽ mang đến cho mọi người 1 giải pháp mà chúng ta có thể nghĩ đến và bước đầu để ứng dụng thử vào hệ thống làm việc của mọi người.

Vắn Quang Quí
PHP Developer

ỨNG TUYỂN







    Chế độ phúc lợi

    CHÍNH SÁCH LƯƠNG & THƯỞNG

    Thấu hiểu tâm tư nguyện vọng của nhân viên, công ty Rivercrane Việt Nam đặc biệt thiết lập chế độ xét tăng lương định kỳ 2lần/năm. Xét đánh giá vào tháng 06 và tháng 12 hàng năm và thay đổi lương vào tháng 01 và tháng 07 hàng năm. Ngoài ra, nhân viên còn được thưởng thành tích định kỳ cho các cá nhân xuất sắc trong tháng, năm.

    CHẾ ĐỘ ĐÀO TẠO TẠI NHẬT

    Luôn luôn mong muốn các kỹ sư và nhân viên trong công ty có cái nhìn toàn diện về lập trình những mảng kỹ thuật trên thế giới, công ty Rivercrane Việt Nam quyết định chế độ 3 tháng 1 lần đưa nhân viên đi học tập tại Nhật. Các bạn kỹ sư hoàn toàn đều có thể quyết định khả năng phát triển bản thân theo hướng kỹ thuật hoặc theo hướng quản lý.

    CHẾ ĐỘ ĐI DU LỊCH HÀNG NĂM

    Không chỉ đưa đến cho nhân viên những công việc thử thách thể hiện bản thân, công ty Rivercrane Việt Nam muốn nhân viên luôn thích thú khi đến với những chuyến hành trình thú vị hàng năm. Những buổi tiệc Gala Dinner sôi động cùng với những trò chơi Team Building vui nhộn sẽ giúp cho đại gia đình Rivercrane thân thiết hơn.

    CHẾ ĐỘ EVENT CÔNG TY

    Những hoạt động Team building, Company Building, Family Building, Summer Holiday, Mid-Autumn Festival… sẽ là những khoảnh khắc gắn kết đáng nhớ của mỗi một nhân viên trong từng dự án, hoặc sẽ là những điều tự hào khi giới thiệu công ty mình với với gia đình thân thương, cùng nhau chia sẻ yêu thương với thông điệp “We are One”

    BẢO HIỂM

    Công ty Rivercrane Việt Nam đảm bảo tham gia đầy đủ chế độ Bảo hiểm xã hội, bảo hiểm y tế và bảo hiểm thất nghiệp. Cam kết chặt chẽ về mọi thủ tục phát sinh công ty đều hỗ trợ và tiến hành cho nhân viên từ đầu đến cuối. Những chế độ bảo hiểm khác công ty cũng đặc biệt quan tâm và từng bước tiến hành.

    CHẾ ĐỘ PHÚC LỢI KHÁC

    Hỗ trợ kinh phí cho các hoạt động văn hóa, văn nghệ, thể thao; Hỗ trợ kinh phí cho việc mua sách nghiên cứu kỹ thuật; Hỗ trợ kinh phí thi cử bằng cấp kỹ sư, bằng cấp dành cho ngôn ngữ. Hỗ trợ kinh phí tham gia các lớp học về quản lý kỹ thuật bên ngoài; Các hỗ trợ phúc lợi khác theo quy định công ty…

    CÔNG VIỆC TƯƠNG TỰ

    © 2012 RiverCrane Vietnam. All rights reserved.

    Close