Viết REST API với Express trong Node.js
1. Tổng quan
Bài này hướng dẫn bạn xây dựng một REST API quản lý Order bằng Express — từ khởi tạo project, định nghĩa route, middleware, validate dữ liệu, đến xử lý lỗi tập trung. Đọc xong, bạn có thể tự dựng một API CRUD hoàn chỉnh, có test tự động bằng Jest và Supertest, và biết những lỗi thường gặp khi mới làm việc với Express.
Ví dụ xuyên suốt bài: API quản lý đơn hàng (Order) của một Customer, hỗ trợ tạo, xem, sửa, xóa.
2. Chuẩn bị dự án
Tạo project Node.js mới và cài Express:
mkdir order-api && cd order-api
npm init -y
npm install express
npm install --save-dev jest supertest
Thêm type: module để dùng cú pháp import/export (ES Module), và khai báo script test trong package.json:
{
"name": "order-api",
"type": "module",
"scripts": {
"start": "node src/server.js",
"test": "node --experimental-vm-modules node_modules/.bin/jest"
}
}
Lưu ý: Jest với ES Module cần flag
--experimental-vm-modules. Nếu không muốn rắc rối, bạn có thể bỏ"type": "module"và dùngrequire/module.exports(CommonJS) — cả hai đều hợp lệ, tùy sở thích team.
3. Tạo server Express cơ bản
// src/app.js
import express from 'express';
const app = express();
app.use(express.json()); // parse JSON body thành req.body
app.get('/health', (req, res) => {
res.json({ status: 'UP' });
});
export default app;
// src/server.js
import app from './app.js';
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Order API đang chạy tại port ${PORT}`);
});
express.json()là middleware tích hợp sẵn, giúp Express tự parseContent-Type: application/jsonthành objectreq.body. Không có dòng này,req.bodysẽ làundefined.- Tách
app.jskhỏiserver.jsđể test không cần thực sự mở port — đây là thói quen quan trọng cho REST API Node.js.
Chạy thử:
node src/server.js
curl http://localhost:3000/health
{"status":"UP"}
4. Định nghĩa resource Order
Dùng một “database” in-memory bằng array để tập trung vào phần API, không phụ thuộc DB thật.
// src/orders/orderRepository.js
let orders = [];
let nextId = 1;
export function findAll() {
return orders;
}
export function findById(id) {
return orders.find((o) => o.id === id);
}
export function create(data) {
const order = { id: nextId++, ...data, status: 'PENDING' };
orders.push(order);
return order;
}
export function update(id, data) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return null;
orders[index] = { ...orders[index], ...data };
return orders[index];
}
export function remove(id) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return false;
orders.splice(index, 1);
return true;
}
Repository này mô phỏng các thao tác CRUD mà bạn sẽ thấy ở bất kỳ ORM nào (Sequelize, Prisma…), chỉ khác là lưu trong RAM thay vì DB.
5. Routing với Express Router
Thay vì gắn hết route vào app, dùng express.Router() để module hóa theo resource:
// src/orders/orderRouter.js
import { Router } from 'express';
import * as repo from './orderRepository.js';
const router = Router();
router.get('/', (req, res) => {
res.json(repo.findAll());
});
router.get('/:id', (req, res, next) => {
const order = repo.findById(Number(req.params.id));
if (!order) return next({ status: 404, message: 'Order không tồn tại' });
res.json(order);
});
router.post('/', (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
router.put('/:id', (req, res, next) => {
const order = repo.update(Number(req.params.id), req.body);
if (!order) return next({ status: 404, message: 'Order không tồn tại' });
res.json(order);
});
router.delete('/:id', (req, res, next) => {
const deleted = repo.remove(Number(req.params.id));
if (!deleted) return next({ status: 404, message: 'Order không tồn tại' });
res.status(204).end();
});
export default router;
Gắn router vào app với tiền tố /orders:
// src/app.js (cập nhật)
import orderRouter from './orders/orderRouter.js';
app.use('/orders', orderRouter);
router.get('/:id', ...)—:idlà route param, lấy quareq.params.id(luôn là string, cầnNumber()để so sánh).- Gọi
next({ status: 404, message: ... })chuyển lỗi sang middleware xử lý lỗi (mục 7), thay vì viếtres.status(404)...lặp lại ở mỗi route. 204 No Contentcho DELETE thành công — không trả body.
6. Validate dữ liệu đầu vào
Validate tay bằng if/else rất dễ sót case. Dùng express-validator cho gọn và rõ ràng:
npm install express-validator
// src/orders/orderValidator.js
import { body, validationResult } from 'express-validator';
export const validateCreateOrder = [
body('customerName').isString().notEmpty().withMessage('customerName là bắt buộc'),
body('amount').isFloat({ gt: 0 }).withMessage('amount phải lớn hơn 0'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
Gắn validator vào route POST:
// src/orders/orderRouter.js (cập nhật)
import { validateCreateOrder } from './orderValidator.js';
router.post('/', validateCreateOrder, (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
- Mỗi
body(...)là một rule kiểm tra một field; nếu sai, lỗi được gom lại trongvalidationResult(req). - Middleware cuối cùng trong array kiểm tra
errors.isEmpty()— nếu có lỗi, trả400kèm chi tiết, không gọinext()để chặn request đi tiếp vào handler thật.
Gửi request thiếu amount:
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"customerName": "Nam"}'
{"errors":[{"type":"field","msg":"amount phải lớn hơn 0","path":"amount",...}]}
7. Xử lý lỗi tập trung
Express cho phép định nghĩa error-handling middleware — là middleware có 4 tham số (err, req, res, next), đặt sau tất cả route:
// src/errorHandler.js
export function errorHandler(err, req, res, next) {
const status = err.status || 500;
const message = err.message || 'Lỗi hệ thống';
if (status === 500) {
console.error(err); // log lỗi thật để debug, không trả stack trace cho client
}
res.status(status).json({ message });
}
// src/app.js (cập nhật, thêm vào CUỐI file)
import { errorHandler } from './errorHandler.js';
app.use(errorHandler);
- Express nhận ra middleware này là error handler nhờ số lượng tham số (4), không phải tên hàm.
- Phải đặt
app.use(errorHandler)sau cùng, sau mọiapp.use(router)— Express xử lý middleware theo thứ tự khai báo. - Mọi
next(err)hoặc lỗi throw trong route (nếu dùngexpress-async-errorshoặc try/catch) sẽ rơi vào đây.
Lỗi hay gặp: quên gọi
next(err)trong code async (ví dụawait db.query(...)throw) khiến Express không bắt được lỗi, server crash hoặc request treo. Với Express 5 (từ bản mới), async handler throw lỗi tự động được bắt — còn Express 4 bạn cần bọc try/catch và gọinext(err)thủ công.
8. Thêm tầng Service để tách logic nghiệp vụ
Khi logic phức tạp hơn (ví dụ: không cho tạo order nếu amount vượt hạn mức khách hàng), đừng nhồi hết vào router. Tách ra orderService:
// src/orders/orderService.js
import * as repo from './orderRepository.js';
const MAX_AMOUNT = 100_000_000;
export function createOrder(data) {
if (data.amount > MAX_AMOUNT) {
const err = new Error('Vượt hạn mức cho phép');
err.status = 422;
throw err;
}
return repo.create(data);
}
// src/orders/orderRouter.js (cập nhật route POST)
import * as orderService from './orderService.js';
router.post('/', validateCreateOrder, (req, res, next) => {
try {
const order = orderService.createOrder(req.body);
res.status(201).json(order);
} catch (err) {
next(err);
}
});
Router giờ chỉ lo HTTP (status code, parse param), còn orderService lo nghiệp vụ. Đây là pattern giống layer Controller–Service trong các framework khác, giúp test logic nghiệp vụ độc lập với HTTP.
9. Viết test với Jest và Supertest
Supertest cho phép gọi API ngay trên object app (Express instance) mà không cần mở port thật — đây là lý do mục 3 tách app.js khỏi server.js.
// tests/order.test.js
import request from 'supertest';
import app from '../src/app.js';
describe('Order API', () => {
it('tạo order thành công trả về 201 và id', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam', amount: 500000 });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
customerName: 'Nam',
amount: 500000,
status: 'PENDING',
});
expect(response.body.id).toBeDefined();
});
it('thiếu amount trả về 400', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam' });
expect(response.status).toBe(400);
});
it('lấy order không tồn tại trả về 404', async () => {
const response = await request(app).get('/orders/9999');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Order không tồn tại');
});
});
Chạy test:
npm test
PASS tests/order.test.js
Order API
✓ tạo order thành công trả về 201 và id
✓ thiếu amount trả về 400
✓ lấy order không tồn tại trả về 404
request(app)khởi tạo một HTTP client ảo gắn trực tiếp vào Express app — nhanh hơn test qua network thật.toMatchObjectchỉ kiểm tra các field chỉ định, bỏ qua field khác (nhưidtự sinh) — tiện cho test response có giá trị động.
Lưu ý: vì
orderslà array in-memory dùng chung giữa các test, test chạy theo thứ tự có thể ảnh hưởng nhau (ví dụ order tạo ở test 1 vẫn còn khi test 2 chạy). Với API thật nối DB, hãy reset dữ liệu trongbeforeEachhoặc dùng transaction rollback.
10. Khi nào không nên dùng Express thuần
- API lớn, nhiều domain phức tạp: Express không có convention bắt buộc về cấu trúc, dễ sinh “spaghetti code” nếu team không tự quy ước. Cân nhắc NestJS (có DI, module, decorator giống Spring) nếu team quen kiến trúc layered rõ ràng.
- Cần type-safety mạnh: Express + JavaScript thuần không bắt lỗi kiểu dữ liệu lúc compile. Nếu project lớn, nên dùng TypeScript ngay từ đầu, không đợi “lớn rồi convert” — chi phí convert sau này cao hơn nhiều.
- High-throughput, cần streaming/WebSocket phức tạp: Express vẫn chạy được nhưng Fastify hoặc các framework tối ưu hiệu năng hơn (benchmark JSON serialize nhanh hơn) có thể phù hợp hơn cho hệ thống traffic cực lớn.
Với API nhỏ–vừa, CRUD rõ ràng như ví dụ trên, Express vẫn là lựa chọn nhanh gọn, cộng đồng lớn, dễ tìm middleware có sẵn (auth, rate-limit, CORS…).
11. Kết luận
Qua bài này, bạn đã dựng được REST API Order hoàn chỉnh với Express: routing bằng Router, validate input bằng express-validator, xử lý lỗi tập trung bằng middleware 4 tham số, và tách logic nghiệp vụ ra service layer để dễ test. Phần test với Jest + Supertest giúp bạn tự tin refactor mà không sợ phá vỡ hành vi API. Từ nền tảng này, bạn có thể thêm kết nối database thật (Prisma, Mongoose), authentication (JWT), hoặc chuyển sang TypeScript khi project lớn dần.