Givepo
Blog

Giải thích kiến trúc backend: REST API, microservices và cơ sở dữ liệu mở rộng được

Cách thiết kế REST API mà lập trình viên thích dùng, khi nào microservices đáng với chi phí, cách mô hình hóa cơ sở dữ liệu chịu được tải lớn, và những mẫu thiết kế giữ cho hệ thống backend nhanh và ổn định.

Đội ngũ Givepo
6 phút đọc
Illustration of a client sending a GET request to an API gateway that connects to three services and a databaseBackend & API

Mọi ứng dụng mang lại cảm giác nhanh và đáng tin cậy đều có một backend được thiết kế tốt phía sau. Backend nắm giữ các quy tắc nghiệp vụ, bảo vệ dữ liệu và trả lời hàng nghìn request mỗi giây mà không ai nhận ra. Làm đúng kiến trúc từ sớm giúp tiết kiệm hàng tháng làm lại về sau, còn làm sai sẽ lộ ra thành sự cố, trang chậm và những tính năng tốn thời gian hơn nhiều so với cần thiết.

Bài viết này đi qua những quyết định quan trọng nhất: thiết kế API, cấu trúc dịch vụ, mô hình hóa dữ liệu và giữ mọi thứ vận hành ổn định khi lưu lượng tăng lên.

Thiết kế REST API mà mọi người thích dùng

REST mô hình hóa hệ thống thành các tài nguyên (danh từ) mà client thao tác bằng các phương thức HTTP chuẩn (động từ). Sự nhất quán là cốt lõi: khi lập trình viên đã học một endpoint, họ có thể đoán được phần còn lại.

  • GET /v1/orders liệt kê đơn hàng, với lọc và phân trang qua tham số truy vấn.
  • GET /v1/orders/{id} trả về một đơn hàng.
  • POST /v1/orders tạo đơn hàng và trả về 201 Created cùng header Location.
  • PATCH /v1/orders/{id} cập nhật một phần đơn hàng; PUT thay thế toàn bộ.
  • DELETE /v1/orders/{id} xóa đơn hàng và trả về 204 No Content.

Mã trạng thái là một phần trong hợp đồng của bạn, vì vậy hãy dùng chúng chính xác:

  • 200 OK, 201 Created và 204 No Content cho thành công.
  • 400 Bad Request cho request sai định dạng và 422 Unprocessable Content cho request đúng định dạng nhưng không hợp lệ.
  • 401 Unauthorized khi người gọi chưa xác thực, và 403 Forbidden khi đã xác thực nhưng không có quyền.
  • 404 Not Found, 409 Conflict cho xung đột trạng thái như bản ghi trùng lặp, và 429 Too Many Requests khi bị giới hạn tần suất.
  • 5xx chỉ dành cho lỗi phía server, không bao giờ dùng cho dữ liệu đầu vào sai.

Đây là một endpoint TypeScript dùng Express, kiểm tra dữ liệu đầu vào ngay tại ranh giới và trả về đúng mã trạng thái:

TypeScript
import express from 'express'
import { z } from 'zod'
import { orders } from './orders'

const app = express()
app.use(express.json())

const CreateOrder = z.object({
  customerId: z.string().uuid(),
  items: z.array(z.object({ sku: z.string().min(1), quantity: z.number().int().positive() })).min(1),
})

app.post('/v1/orders', async (req, res) => {
  const parsed = CreateOrder.safeParse(req.body)
  if (!parsed.success) {
    return res.status(422).json({
      type: 'https://api.example.com/problems/validation',
      title: 'The request body is invalid',
      status: 422,
      errors: parsed.error.issues.map((issue) => ({ path: issue.path.join('.'), message: issue.message })),
    })
  }

  const order = await orders.create(parsed.data)
  res.status(201).location(`/v1/orders/${order.id}`).json(order)
})

Lỗi nhất quán, phân trang và quản lý phiên bản

Trả về mọi lỗi theo cùng một cấu trúc để client có thể xử lý chung. Định dạng Problem Details (RFC 9457) là một chuẩn được hỗ trợ rộng rãi:

JSON
{
  "type": "https://api.example.com/problems/out-of-stock",
  "title": "Item is out of stock",
  "status": 409,
  "detail": "SKU TS-1042 has 0 units available; 3 were requested.",
  "instance": "/v1/orders"
}
  • Phân trang: ưu tiên phân trang dựa trên con trỏ (?after=ord_123&limit=50) cho các tập dữ liệu lớn hoặc thay đổi nhanh. Phân trang theo offset sẽ bỏ sót hoặc lặp lại mục khi có dòng mới được chèn trong lúc client đang duyệt.
  • Quản lý phiên bản: đặt phiên bản chính trong đường dẫn (/v1/) và chỉ thêm trường mới trong cùng một phiên bản. Xóa hoặc đổi tên trường là thay đổi phá vỡ tương thích.
  • Tính idempotent: cho phép client gửi header Idempotency-Key với request POST, một mẫu được các API thanh toán phổ biến, để request gửi lại không bao giờ tạo thêm một đơn hàng hay một lần thanh toán nữa.

Monolith hay microservices?

Monolith là một ứng dụng triển khai duy nhất. Microservices chia hệ thống thành các dịch vụ nhỏ, mỗi dịch vụ sở hữu dữ liệu riêng và được triển khai độc lập. Microservices thường được giới thiệu như lựa chọn mặc định hiện đại, nhưng chúng đổi độ phức tạp của code lấy độ phức tạp vận hành:

  • Bạn được: triển khai độc lập, mở rộng riêng các điểm nóng, và các đội làm việc mà không giẫm chân lên nhau.
  • Bạn phải trả: các lời gọi mạng có thể thất bại hoặc chậm, dữ liệu phân tán trên nhiều cơ sở dữ liệu không có transaction chung, truy vết và gỡ lỗi phân tán, cùng nhiều hạ tầng hơn để vận hành.

Với hầu hết sản phẩm, điểm khởi đầu đúng là modular monolith: một khối triển khai duy nhất nhưng có ranh giới nội bộ chặt chẽ (đơn hàng, thanh toán, danh mục) chỉ giao tiếp qua các giao diện được định nghĩa. Nếu sau này một module cần mở rộng hoặc phát hành độc lập, ranh giới của nó đã đủ sạch để tách thành một dịch vụ.

Hãy tách hệ thống theo ranh giới nghiệp vụ khi một đội ngũ hoặc nhu cầu mở rộng đòi hỏi, chứ không phải vì sơ đồ kiến trúc trông đẹp hơn.

Khi các dịch vụ cần giao tiếp, hãy dùng lời gọi đồng bộ (REST hoặc gRPC) khi bên gọi cần câu trả lời ngay, và sự kiện bất đồng bộ qua message broker khi các dịch vụ khác chỉ cần phản ứng: "đã đặt hàng", "đã nhận thanh toán". Để phát sự kiện một cách tin cậy, hãy ghi chúng vào một bảng outbox trong cùng transaction với thay đổi nghiệp vụ và để một tiến trình riêng chuyển tiếp chúng tới broker. Nếu không, một sự cố giữa hai bước sẽ khiến dữ liệu và sự kiện của bạn mất đồng bộ.

Thiết kế cơ sở dữ liệu có thể mở rộng

Phần lớn vấn đề hiệu năng là vấn đề cơ sở dữ liệu. Một vài nguyên tắc mang lại hiệu quả lớn:

  • Mô hình hóa nghiệp vụ trước, rồi mới đến truy vấn. Bắt đầu với dạng chuẩn hóa để mỗi dữ kiện chỉ nằm ở một nơi, và phi chuẩn hóa có chủ đích ở những chỗ một truy vấn đã được đo đạc cần đến.
  • Để cơ sở dữ liệu thực thi các quy tắc: ràng buộc NOT NULL, khóa ngoại, UNIQUE và CHECK bắt được những lỗi mà code ứng dụng bỏ sót.
  • Lưu tiền dưới dạng số nguyên (đơn vị xu) hoặc kiểu decimal, không bao giờ dùng số thực dấu phẩy động.
  • Đánh chỉ mục cho các truy vấn thực tế, và kiểm tra chúng bằng EXPLAIN. Mỗi chỉ mục tăng tốc đọc nhưng làm chậm ghi.
  • Thay đổi schema bằng migration có phiên bản chạy tự động, không bao giờ sửa tay trên production.
SQL
CREATE TABLE orders (
  id          UUID PRIMARY KEY,
  customer_id UUID NOT NULL REFERENCES customers (id),
  status      TEXT NOT NULL CHECK (status IN ('pending', 'paid', 'shipped', 'cancelled')),
  total_cents BIGINT NOT NULL CHECK (total_cents >= 0),
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

-- Serves "latest orders for a customer" without scanning the whole table
CREATE INDEX orders_customer_created_idx ON orders (customer_id, created_at DESC);

Hãy cẩn thận với vấn đề truy vấn N+1: tải 50 đơn hàng rồi chạy thêm một truy vấn cho mỗi đơn để lấy thông tin khách hàng. Thay vào đó, hãy lấy dữ liệu liên quan trong một truy vấn bằng join hoặc WHERE id IN (…) theo lô. Ngoài ra hãy dùng connection pool, vì mở một kết nối cơ sở dữ liệu cho mỗi request vừa chậm vừa nhanh chóng làm cạn tài nguyên server.

Các mẫu mở rộng hiệu quả

  1. Giữ server ứng dụng không trạng thái (phiên lưu trong cơ sở dữ liệu hoặc cache, file trong object storage) để bạn có thể chạy bao nhiêu bản sao tùy ý phía sau bộ cân bằng tải.
  2. Lưu đệm các thao tác đọc tốn kém với thời gian sống ngắn, và quyết định từ đầu cách mỗi mục cache bị vô hiệu hóa khi dữ liệu thay đổi.
  3. Chuyển công việc chậm sang hàng đợi nền: email, báo cáo, xử lý ảnh và lời gọi tới bên thứ ba. API phản hồi ngay và các worker xử lý sau.
  4. Thêm bản sao chỉ đọc (read replica) khi đọc chiếm ưu thế, và gửi các truy vấn báo cáo tới đó thay vì máy chính.
  5. Giới hạn tần suất các endpoint công khai để bảo vệ hệ thống khỏi lạm dụng và các client mất kiểm soát.
  6. Phân vùng hoặc sharding dữ liệu sau cùng, chỉ khi một cơ sở dữ liệu được tinh chỉnh tốt thực sự không theo kịp.

Độ tin cậy và khả năng quan sát

Hệ thống phân tán hỏng theo những cách cục bộ và khó hiểu, vì vậy hãy thiết kế để đối phó. Đặt timeout cho mọi lời gọi ra ngoài. Chỉ thử lại các thao tác idempotent, với exponential backoff và độ trễ ngẫu nhiên (jitter) để các client không thử lại cùng lúc. Dùng circuit breaker để ngừng gọi một dịch vụ phụ thuộc rõ ràng đang ngừng hoạt động.

Sau đó làm cho hệ thống có thể quan sát được: log có cấu trúc kèm request ID, metric cho độ trễ, lưu lượng, lỗi và mức độ bão hòa, cùng distributed tracing, ví dụ với OpenTelemetry, cho thấy thời gian được tiêu tốn ở đâu giữa các dịch vụ. Hãy cảnh báo dựa trên triệu chứng mà người dùng cảm nhận, như tỷ lệ lỗi và độ trễ, thay vì mọi lần CPU tăng đột biến.

Những điểm chính

  • Thiết kế API xoay quanh tài nguyên, mã trạng thái chính xác và một định dạng lỗi nhất quán.
  • Bắt đầu với modular monolith; tách dịch vụ theo ranh giới nghiệp vụ khi có nhu cầu thực sự.
  • Để cơ sở dữ liệu bảo đảm tính toàn vẹn, đánh chỉ mục cho truy vấn thực tế và tránh truy vấn N+1.
  • Mở rộng bằng server không trạng thái, bộ nhớ đệm và hàng đợi, đồng thời xây dựng timeout, cơ chế thử lại và khả năng quan sát ngay từ đầu.

Kết luận

Backend mở rộng được không đến từ công nghệ kỳ lạ. Chúng đến từ những quyết định nhàm chán, nhất quán được áp dụng ở mọi nơi: hợp đồng API rõ ràng, ranh giới sạch giữa các phần của hệ thống, cơ sở dữ liệu được mô hình hóa tốt và những thói quen vận hành giúp phát hiện vấn đề sớm. Làm đúng những điều đó, việc tăng năng lực xử lý chỉ là thay đổi cấu hình chứ không phải viết lại từ đầu.

Đọc thêm

  • #Architecture
  • #Databases
  • #Microservices
  • #REST API
  • #TypeScript
Tác giả
Đội ngũ Givepo

Givepo là công ty IT chuyên xây dựng website, ứng dụng di động và phần mềm theo yêu cầu, giúp doanh nghiệp của bạn phát triển vượt bậc.

Chia sẻ bài viết

Tư vấn miễn phí 30 phút

Trao đổi với đội ngũ của chúng tôi về ý tưởng, phạm vi và ngân sách của bạn.

Đặt lịch tư vấn

Bài viết khác

Blog
Kỹ thuật phần mềm
·6 phút đọc

Clean Architecture và nguyên tắc SOLID: Viết phần mềm dễ bảo trì lâu dài

Vì sao codebase chậm dần khi lớn lên, và cách clean architecture, quy tắc phụ thuộc cùng các nguyên tắc SOLID giữ cho logic nghiệp vụ dễ kiểm thử và dễ thay đổi, kèm một ví dụ TypeScript hoàn chỉnh.

Xem chi tiết
Phát triển web
·5 phút đọc

Phát triển web hiện đại với Next.js, React và TypeScript: Các phương pháp tốt nhất

Server Components, chiến lược render, dữ liệu an toàn kiểu và Core Web Vitals. Hướng dẫn thực tế để xây dựng ứng dụng web nhanh, dễ bảo trì với bộ công nghệ mà hầu hết các đội đang chọn.

Xem chi tiết
AI & Học máy
·7 phút đọc

AI và Machine Learning trong thực tế: Mang lại giá trị kinh doanh ở đâu

Gạt bỏ những lời quảng cáo thổi phồng. Hiểu cách machine learning và AI tạo sinh thực sự hoạt động, chúng tạo ra giá trị đo lường được ở đâu, và cách triển khai dự án AI đầu tiên đến tay người dùng thay vì dừng lại ở bản demo.

Xem chi tiết