Xây dựng REST API với Node.js và Express: Hướng dẫn sẵn sàng cho môi trường sản phẩm

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Bối cảnh & Lý do: Kỹ thuật để đạt ổn định

Đồng hồ điểm 2 giờ sáng. Thiết bị báo động của bạn vang lên tiếng chuông đáng sợ. Một dịch vụ quan trọng, một phần cốt lõi của hạ tầng ứng dụng, đã ngừng hoạt động. Khách hàng đang phàn nàn, và thời gian không chờ đợi. Bạn vội vã đến thiết bị đầu cuối, tim đập thình thịch, cố gắng tìm ra nguyên nhân. Nghe quen không? Đó là một kịch bản mà nhiều người trong chúng ta đã trải qua, và thường thì nguyên nhân là do các API được thiết kế kém hoặc quá mong manh.

Xây dựng một REST API mạnh mẽ, có khả năng mở rộng và dễ bảo trì không chỉ đơn thuần là viết code; đó là việc tạo ra một huyết mạch cho các ứng dụng của bạn. Sau những sự cố kinh hoàng lúc 2 giờ sáng đó, tôi luôn nhận thấy rằng một API có cấu trúc tốt, dễ dự đoán và được xây dựng trên nền tảng vững chắc sẽ dễ dàng khắc phục sự cố và khôi phục hoạt động hơn nhiều. Chính vì lý do này mà tôi rất tin tưởng vào Node.js và Express cho nhiều dịch vụ backend của mình.

Node.js, với kiến trúc bất đồng bộ, hướng sự kiện, vượt trội trong việc xử lý nhiều kết nối đồng thời – chính xác là những gì một API có lưu lượng truy cập cao yêu cầu. Express.js, một framework web tối giản cho Node.js, cung cấp các công cụ cần thiết để xây dựng các API mạnh mẽ mà không bị sa lầy vào sự phức tạp quá mức.

Nó cung cấp đủ cấu trúc để hoàn thành công việc một cách hiệu quả, nhưng không áp đặt mọi lựa chọn thiết kế. Sự cân bằng này rất quan trọng để nhanh chóng phát triển các dịch vụ hoạt động đáng tin cậy.

Tôi đã áp dụng phương pháp này trong các môi trường sản phẩm, và kết quả luôn ổn định. Khi một API cung cấp sự rõ ràng, tuân thủ các quy ước đã thiết lập và áp dụng các thực hành ghi log thông minh, những cuộc gọi lúc 2 giờ sáng đáng sợ đó sẽ trở nên ít thường xuyên hơn đáng kể và ít căng thẳng hơn nhiều. Sự thay đổi này cho phép các nhóm tập trung vào việc giải quyết các vấn đề kinh doanh thực sự, thay vì liên tục săn lùng các lỗi khó hiểu trong cơ sở hạ tầng của họ.

Cài đặt: Bắt tay vào việc

Được rồi, hãy cùng bạn thiết lập để xây dựng API sẵn sàng cho môi trường sản phẩm đầu tiên của mình. Trước khi chúng ta đi sâu vào code, hãy đảm bảo bạn đã cài đặt Node.js và npm (Node Package Manager). Nếu chưa, hãy truy cập trang web chính thức của Node.js và tải xuống phiên bản LTS (Long Term Support). Tin tôi đi, dành vài phút cho bước này bây giờ có thể giúp bạn tiết kiệm hàng giờ đau đầu về các phụ thuộc sau này.

Đầu tiên, chúng ta cần một thư mục dự án mới.

mkdir my-first-api
cd my-first-api
npm init -y

npm init -y tạo một tệp package.json với các giá trị mặc định. Tệp này đóng vai trò là bản kê khai cho dự án Node.js của bạn, liệt kê các phụ thuộc, tập script và các siêu dữ liệu dự án khác. Hãy coi nó như bản thiết kế cho ứng dụng của bạn.

Tiếp theo, chúng ta cài đặt Express, trái tim của API của chúng ta.

npm install express dotenv body-parser

Chính xác thì các gói khác này làm gì?

  • express: Framework web cốt lõi của chúng ta.
  • dotenv: Rất quan trọng để quản lý các biến môi trường. Chúng ta không bao giờ nên mã hóa cứng các thông tin nhạy cảm như khóa API hoặc thông tin đăng nhập cơ sở dữ liệu trực tiếp vào codebase của mình, đặc biệt là đối với các triển khai sản phẩm.
  • body-parser: Middleware này giúp Express phân tích các phần thân yêu cầu đến (như dữ liệu JSON hoặc URL-encoded), giúp chúng ta dễ dàng truy cập dữ liệu được gửi từ máy khách.

Tệp package.json của bạn giờ đây sẽ phản ánh các phụ thuộc mới được thêm vào này:

{
  "name": "my-first-api",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "node index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "dotenv": "^16.4.5",
    "express": "^4.19.2"
  }
}

Bây giờ, hãy tạo một tệp có tên index.js trong thư mục gốc dự án của bạn. Đây sẽ là tệp ứng dụng chính của chúng ta.

Cấu hình: Định hình API của chúng ta

Sau khi đã đặt nền móng, hãy tiến hành cấu hình API của chúng ta. Mở index.js và thêm thiết lập cơ bản sau:

// index.js
require('dotenv').config(); // Tải các biến môi trường trước

const express = require('express');
const bodyParser = require('body-parser');
const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(bodyParser.json()); // Để phân tích phần thân yêu cầu JSON

// Kho lưu trữ dữ liệu đơn giản trong bộ nhớ để minh họa
let items = [
    { id: '1', name: 'Laptop', description: 'Powerful computing machine' },
    { id: '2', name: 'Keyboard', description: 'Mechanical keyboard with RGB' }
];

// Routes - Các tuyến đường
// Lấy tất cả các mục
app.get('/api/items', (req, res) => {
    res.json(items);
});

// Lấy một mục duy nhất bằng ID
app.get('/api/items/:id', (req, res) => {
    const item = items.find(i => i.id === req.params.id);
    if (item) {
        res.json(item);
    } else {
        res.status(404).send('Không tìm thấy mục');
    }
});

// POST một mục mới
app.post('/api/items', (req, res) => {
    const newItem = {
        id: String(items.length + 1), // Tạo ID đơn giản cho mục đích demo
        name: req.body.name,
        description: req.body.description
    };
    if (newItem.name && newItem.description) {
        items.push(newItem);
        res.status(201).json(newItem); // 201 Created
    } else {
        res.status(400).send('Cần có tên và mô tả');
    }
});

// PUT để cập nhật một mục
app.put('/api/items/:id', (req, res) => {
    const itemIndex = items.findIndex(i => i.id === req.params.id);
    if (itemIndex > -1) {
        items[itemIndex] = { ...items[itemIndex], ...req.body };
        res.json(items[itemIndex]);
    } else {
        res.status(404).send('Không tìm thấy mục');
    }
});

// DELETE một mục
app.delete('/api/items/:id', (req, res) => {
    const initialLength = items.length;
    items = items.filter(i => i.id !== req.params.id);
    if (items.length < initialLength) {
        res.status(204).send(); // 204 No Content
    } else {
        res.status(404).send('Không tìm thấy mục');
    }
});

// Xử lý lỗi cơ bản (luôn nên có)
app.use((err, req, res, next) => {
    console.error(err.stack);
    res.status(500).send('Đã xảy ra lỗi!');
});

// Khởi động máy chủ
app.listen(PORT, () => {
    console.log(`Máy chủ đang chạy trên cổng ${PORT}`);
});

Hãy cùng phân tích những gì đang diễn ra ở đây:

  • require('dotenv').config();: Dòng này đọc bất kỳ cặp khóa-giá trị nào từ tệp .env, tải chúng vào process.env. Bạn sẽ cần tạo một tệp .env trong thư mục gốc của dự án của mình:
# .env
PORT=4000

Thiết lập này cho phép dễ dàng sửa đổi cổng của ứng dụng mà không cần thay đổi mã nguồn, điều này rất có giá trị khi triển khai đến các môi trường khác nhau (như phát triển, thử nghiệm hoặc sản phẩm). Đáng chú ý, nếu PORT không được chỉ định trong tệp `.env`, ứng dụng sẽ tự động sử dụng cổng `3000`.

  • app.use(bodyParser.json());: Đây là middleware của chúng ta. Bất kỳ yêu cầu đến nào có tiêu đề Content-Type: application/json sẽ có phần thân được tự động phân tích thành một đối tượng JavaScript, sau đó có sẵn trên req.body.
  • Routes (app.get, app.post, app.put, app.delete): Các tuyến đường này định nghĩa các điểm cuối API và chỉ định cách chúng phản hồi với các phương thức HTTP khác nhau. Chúng ta đã triển khai các hoạt động CRUD (Tạo, Đọc, Cập nhật, Xóa) cơ bản cho một tài nguyên items đơn giản. Hãy chú ý cách `req.params.id` truy xuất các tham số từ URL, trong khi `req.body` truy cập dữ liệu được gửi trong phần thân yêu cầu.
  • Mã trạng thái HTTP: Điều quan trọng là phải chú ý đến các mã trạng thái HTTP. Chẳng hạn, `200 OK` biểu thị việc đọc hoặc cập nhật thành công, `201 Created` xác nhận tạo tài nguyên thành công và `204 No Content` cho biết việc xóa thành công. Đối với lỗi phía máy khách, `404 Not Found` hoặc `400 Bad Request` thường được sử dụng. Các mã này rất quan trọng để các ứng dụng máy khách diễn giải chính xác kết quả yêu cầu của họ.
  • Xử lý lỗi: Khối `app.use((err, req, res, next) => { … })` đóng vai trò là middleware xử lý lỗi Express cơ bản. Mọi lỗi phát sinh từ các tuyến đường hoặc middleware khác của chúng ta cuối cùng sẽ được bắt ở đây. Điều này ngăn máy chủ của bạn bị treo và cung cấp cơ hội để ghi log lỗi đồng thời gửi phản hồi có ý nghĩa đến máy khách. Thiết lập đơn giản này có thể là một vị cứu tinh thực sự khi gỡ lỗi vào giữa đêm.

Xác minh & Giám sát: Đảm bảo ổn định

Một API chạy mà không có kiểm thử hoặc giám sát phù hợp về cơ bản là hoạt động mù quáng. Chúng ta cần xác minh các điểm cuối của mình hoạt động như mong đợi và đảm bảo chúng ta có cái nhìn rõ ràng về tình trạng và hiệu suất của nó. Phương pháp tiếp cận chủ động này là điều giúp ngăn chặn những cuộc gọi khẩn cấp lúc 2 giờ sáng đó.

Kiểm thử các điểm cuối của chúng ta

Trước khi xem xét triển khai, hãy gửi một số yêu cầu đến API của chúng ta. Bạn có thể sử dụng các công cụ dòng lệnh như curl, một tiện ích mở rộng trình duyệt như Postman hoặc Insomnia, hoặc thậm chí là API fetch tích hợp sẵn trực tiếp từ bảng điều khiển nhà phát triển của trình duyệt của bạn.

Đầu tiên, hãy khởi động máy chủ của bạn:

npm start

Bạn sẽ thấy một thông báo cho biết Máy chủ đang chạy trên cổng 4000 (hoặc bất kỳ cổng nào bạn đã cấu hình).

1. Lấy tất cả các mục:

curl http://localhost:4000/api/items

Đầu ra mong đợi:

[
    { "id": "1", "name": "Laptop", "description": "Powerful computing machine" },
    { "id": "2", "name": "Keyboard", "description": "Mechanical keyboard with RGB" }
]

2. Lấy một mục duy nhất:

curl http://localhost:4000/api/items/1

Đầu ra mong đợi:

{ "id": "1", "name": "Laptop", "description": "Powerful computing machine" }

3. POST một mục mới:

curl -X POST -H "Content-Type: application/json" -d '{"name": "Mouse", "description": "Ergonomic wireless mouse"}' http://localhost:4000/api/items

Đầu ra mong đợi (với mã trạng thái 201 Created):

{ "id": "3", "name": "Mouse", "description": "Ergonomic wireless mouse" }

Bây giờ, nếu bạn thực hiện lại yêu cầu GET tất cả các mục, bạn sẽ thấy mục mới được thêm vào trong danh sách.

4. PUT để cập nhật một mục:

curl -X PUT -H "Content-Type: application/json" -d '{"description": "Updated description for Laptop"}' http://localhost:4000/api/items/1

Đầu ra mong đợi:

{ "id": "1", "name": "Laptop", "description": "Updated description for Laptop" }

5. XÓA một mục:

curl -X DELETE http://localhost:4000/api/items/2

Đầu ra mong đợi (một phản hồi trống với mã trạng thái 204 No Content). Xác minh điều này bằng cách thực hiện một cuộc gọi GET tất cả các mục khác.

Ghi log và xử lý lỗi

Mặc dù xử lý lỗi hiện tại của chúng ta hoạt động tốt cho một thiết lập cơ bản, một môi trường sản phẩm yêu cầu những thông tin chi tiết hơn. Hãy cân nhắc tích hợp một thư viện ghi log chuyên dụng như Winston hoặc Pino. Đối với việc ghi log yêu cầu đơn giản, morgan là một lựa chọn tuyệt vời:

Đầu tiên, cài đặt nó:

npm install morgan

Sau đó, tích hợp nó vào tệp index.js của bạn, lý tưởng nhất là trước các tuyến đường của bạn:

Share: