Xử lý lỗi cho Express route

Xử lý lỗi cho Express route
Photo by Gerd Altmann from Pixabay

Express là một framework rất tốt, gọn nhẹ và hiệu quả của Node.js. Thế nhưng đang tồn tại một vấn đề, đó là các lập trình viên Express thường copy-paste những đoạn code chung chung (trả về kết quả, xử lý khi lỗi xảy ra) cho từng route. Trong bài viết này, tôi sẽ trình bày phương pháp khác hiệu quả hơn, có thể áp dụng với các ứng dụng cho hàng trăm route khác nhau.

Ví dụ một project Express

Trước hết, chúng ta sẽ bắt đầu với một ví dụ đơn giản. Có nhiều cách khác nhau để khởi tạo và tổ chức một dự án Express. Tôi sử dụng express-generator để khởi tạo một project mới. Và sau khi loại bỏ những thành phần không dùng đến, thì cấu trúc một project điển hình sẽ thường như thế này:

sample-project
├── bin
|   └── start.js
├── routes
|   └── users.js
├── services
|   └── userService.js
├── app.js
└── package.json

Dưới đây là nội dung của file app.js, là file chính được gọi đầu tiên của ứng dụng Express:

const createError  = require('http-errors');
const express = require('express');
const cookieParser = require('cookie-parser');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/users', usersRouter);
app.use(function(req, res, next) {
    next(createError(404));
});
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.send(err);
});

module.exports = app;

Nội dung file này đơn giản là chúng ta sẽ khởi tạo một app Express và thêm các middleware cơ bản như JSON, URL encoding, cookie. Chúng ta thêm một usersRouter vào /users. Cuối cùng là xử lý lỗi trong trường hợp không có route nào được gọi.

Các file khác thì cơ bản quá rồi, tôi không có gì để nói thêm nữa. Giờ đây, để cài đặt các logic liên quan đến user, tôi sẽ thêm các đường dẫn mới và logic xử lý chúng ở file routes/users.js:

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
    userService.getAll()
        .then(result => res.status(200).send(result))
        .catch(err => res.status(500).send(err));
});

router.get('/:id', function(req, res) {
    userService.getById(req.params.id)
        .then(result => res.status(200).send(result))
        .catch(err => res.status(500).send(err));
});

module.exports = router;

Vấn đề về route của Express

Nhìn vào ví dụ trên, có lẽ mọi người đều dễ dàng nhận ra rằng, mỗi lần gọi userService sẽ luôn kèm với những câu .then.catch lặp đi lặp lại.

Mới nhìn thì có thể đây không phải là vấn đề gì quá lớn. Thế nhưng các ứng dụng trên thực tế không chỉ có một vài route như ví dụ này, mà nó sẽ có hàng chục, thậm chí hàng trăm route khác nhau. Và lặp đi lặp lại một đoạn code cả trăm lần rõ ràng là một vấn đề lớn.

Giải quyết vấn đề

Có rất nhiều cách khác nhau để khắc phục vấn đề trên. Trong phần này, tôi sẽ trình bày một số cách mà mình đã dùng trong các dự án.

Dùng hàm và tái sử dụng

Chúng ta có thể định nghĩa các hàm tiện ích để có thể tái sử dụng trong project. Như ví dụ của chúng ta, tôi sẽ định nghĩa hai hàm để xử lý logic cho trường hợp thông thường và trường hợp lỗi:

// định nghĩa hàm này ở đâu đó để dùng lại
export const handleResponse = (res, data) => res.status(200).send(data);
export const handleError = (res, err) => res.status(500).send(err);

// routes/users.js
router.get('/', function(req, res) {
    userService.getAll()
        .then(data => handleResponse(res, data))
        .catch(err => handleError(res, err));
});

router.get('/:id', function(req, res) {
    userService.getById(req.params.id)
        .then(data => handleResponse(res, data))
        .catch(err => handleError(res, err));
});

Trong code đã gọn gàng hơn rất nhiều. Chúng ta không cần lặp đi lặp lại những đoạn code giống nhau. Thế nhưng một điều bất tiện là chúng ta cần import các hàm tiện ích này và thêm nó vào câu gọi promise tương ứng.

Để khắc phục vấn đề này, có thể dùng đến phương án thứ hai dưới đây.

Middleware

Phương án này là sử dụng best practice liên quan đến promise khi phát triển ứng dụng Express: đưa logic xử lý lỗi vào middleware. Như với ví dụ của chúng ta, triển khai việc này đơn giản như sau:

app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.send(err);
});

Express sẽ hiểu middleware này để xử lý lỗi, bởi vì hàm này có 4 tham số. Giờ đây, với middleware này, chúng ta có thể đẩy việc xử lý lỗi này cho middleware bằng cách gọi next như sau:

// định nghĩa hàm này ở đâu đó để dùng lại
export const handleResponse = (res, data) => res.status(200).send(data);

router.get('/', function(req, res, next) {
    userService.getAll()
        .then(data => handleResponse(res, data))
        .catch(next);
});

router.get('/:id', function(req, res, next) {
    userService.getById(req.params.id)
        .then(data => handleResponse(res, data))
        .catch(next);
});

Code đã gọn gàng hơn rất nhiều 👍.

Middleware nâng cao

Phương án trên vẫn hơi “thủ công” vì chúng ta vẫn phải gọi .then.catch cho từng promise. Với những ứng dụng vừa phải, tôi nghĩ cách này đã tương đối ổn. Với những ứng dụng lớn hơn, chúng ta cần phải có những cách tổng quát hơn nữa.

Chúng ta có thể xây dựng một middleware để tổng quát hóa việc gọi .then.catch này như sau:

app.use((req,res,next) => {
    res.process = (p) => {
        let promiseToResolve;
        if (p.then && p.catch) {
            promiseToResolve = p;
        } else if (typeof p === 'function') {
            promiseToResolve = Promise.resolve().then(() => p());
        } else {
            promiseToResolve = Promise.resolve(p);
        }

        return promiseToResolve
            .then((data) => handleResponse(res, data))
            .catch((e) => handleError(res, e));
    };

    return next();
});

Middleware được xây dựng để tổng quát hóa mọi xử lý logic của route. Nó đã có logic để trả kết quả về cho người dùng và xử lý khi có lỗi xảy ra. Việc còn lại là logic để lấy dữ liệu thì mỗi route sẽ có logic riêng và hàm này cần được truyền res.process. Được xây dựng tổng quát hóa, nên chúng ta có thể truyền vào bất cứ thứ gì: promise, hàm (cả hàm thông thường vào hàm async) hay giá trị.

Trong app.js, chúng ta có thể áp dụng middleware này và sửa lại logic xử lý lỗi một chút. Cứ để code như cũ cũng được nhưng thế này trông pro hơn 😄. Lưu ý rằng 2 middleware dùng để xử lý lỗi này vẫn rất cần thiết, bởi vì nó là nơi xử lý lỗi cho các logic khác có thể có.

app.use(function(req, res, next) {
    res.process(Promise.reject(createError(404)));
});
app.use(function(err, req, res, next) {
    res.process(Promise.reject(err));
});

Quay trở lại nội dung chính liên quan đến ví dụ của chúng ta, routes/users.js giờ đây sẽ được triển khai như thế này:

...
router.get('/', function(req, res) {
    res.process(userService.getAll());
});

router.get('/:id', function(req, res) {
    res.process(() => userService.getById(req.params.id));
});
...

Một điểm cần nhắc lại rằng, mặc dù thêm res.process nhưng việc gọi nó là không bắt buộc. Chúng ta hoàn toàn có thể dùng cách làm như phần trước nếu muốn. Và thực tế là có một số trường hợp, chúng ta bắt buộc phải làm như vậy.

Ví dụ trường hợp chúng ta cần redirect sang một URL khác. Lúc này không có gì trả về cho người dùng không thể gọi res.process như trước đây nữa. Chúng ta có thể kết hợp cách như thế này:

router.get('/:id/profilePic', async function (req, res) {
    try {
        // logic lấy url ở đây
        res.redirect(url);
    } catch (e) {
        res.process(Promise.reject(e));
    }
});

Bằng cách này, chúng ta vẫn thực hiện redirect bình thường và việc xử lý lỗi vẫn thông qua res.process nên không có code dư thừa nào ở đây.

Tôi xin lỗi nếu bài viết có bất kỳ typo nào. Nếu bạn nhận thấy điều gì bất thường, xin hãy cho tôi biết.

Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.

Welcome

manhhomienbienthuy

Đây là thế giới của manhhomienbienthuy (naa). Chào mừng đến với thế giới của tôi!

Bài viết liên quan

Bài viết mới

Chuyên mục

Lưu trữ theo năm

Thông tin liên hệ

Cảm ơn bạn đã quan tâm blog của tôi. Nếu có bất điều gì muốn nói, bạn có thể liên hệ với tôi qua các mạng xã hội, tạo discussion hoặc report issue trên Github.