Bài viết gần đây
| Backend là gì? Toàn bộ cách một hệ thống web vận hành phía sau
Được viết bởi Đặng Trí Thanh vào ngày 01/10/2026 lúc 08:40 | 14 lượt xem
Bài viết thuộc loạt kiến thức nền tảng của Hướng Nghiệp Dữ Liệu. Toàn bộ ví dụ trong bài lấy từ một hệ thống thật đang chạy: app đặt lịch cho tiệm nail Vietnam Nails tại Helsinki, viết bằng Express + PostgreSQL. Dành cho người mới bắt đầu học backend và cho người đã viết frontend muốn hiểu nốt nửa còn lại của hệ thống.
Tóm tắt cho người bận
- Backend là phần mềm chạy trên máy chủ, làm bốn việc: xử lý logic, lưu dữ liệu, xác thực người dùng, và trả kết quả cho client.
- Người dùng không bao giờ nhìn thấy backend — họ chỉ thấy kết quả của nó qua giao diện.
- Một request đi qua 8 chặng: DNS, TCP/TLS, reverse proxy, web server, middleware, route, service, database.
- Trong dự án thật của tôi, backend có 141 endpoint và xử lý ví, đặt lịch, tin nhắn, tính lương — những việc frontend không được phép làm.
- Ba lỗi phổ biến nhất của người mới học backend: nhồi hết logic vào controller, không kiểm tra dữ liệu đầu vào, và để lộ dữ liệu nhạy cảm trong response.
- Bài có checklist 12 điểm để tự rà lại backend đang viết.
Mục lục
- Backend là gì?
- Kiến trúc và nguyên lý hoạt động
- Ví dụ thực tế: hệ thống đặt lịch tiệm nail
- Code: từ ví dụ tối giản đến nghiệp vụ thật
- 5 lỗi thường gặp khi học backend
- Security: những thứ phải làm ngay từ đầu
- Performance: ba thứ quyết định tốc độ
- Testing: kiểm thử backend thế nào?
- Deployment: đưa backend lên chạy thật
- Checklist 12 điểm
- Câu hỏi thường gặp
1. Backend là gì?
Backend là toàn bộ phần mềm chạy trên máy chủ (server), chịu trách nhiệm xử lý logic nghiệp vụ, lưu trữ dữ liệu, xác thực người dùng và cung cấp dữ liệu cho giao diện thông qua các API. Nói cách khác: frontend là phần bạn nhìn thấy, backend là phần bạn phụ thuộc vào nhưng không thấy.
Định nghĩa này nghe đơn giản, nhưng nó chứa một hệ quả quan trọng: mọi thứ quan trọng đều nằm ở backend. Giá tiền, số dư ví, quyền hạn, lịch đặt, lịch sử giao dịch — tất cả những dữ liệu mà nếu sai thì doanh nghiệp mất tiền — đều do backend quyết định. Frontend chỉ hiển thị lại.
1.1. Bốn nhiệm vụ cốt lõi
Dù viết bằng ngôn ngữ nào, backend nào cũng làm bốn việc:
| Nhiệm vụ | Nghĩa là gì | Ví dụ thật |
|---|---|---|
| Xử lý logic | Áp quy tắc nghiệp vụ lên dữ liệu | Đặt lịch trùng giờ thì phải chặn; thanh toán bằng ví thì phải trừ tiền |
| Lưu trữ | Đọc/ghi dữ liệu bền vững | Lưu khách hàng, dịch vụ, lịch hẹn vào PostgreSQL |
| Xác thực & phân quyền | Biết ai đang gọi và được phép làm gì | Chỉ quản trị viên được đổi giá dịch vụ; khách chỉ xem được lịch của mình |
| Giao tiếp | Cung cấp API cho frontend và hệ thống khác | Trả JSON cho app React, gửi dữ liệu cho đối tác qua API tích hợp |
1.2. Phân biệt frontend và backend
Hai khái niệm này hay bị trộn lẫn. Bảng dưới đây tách rõ:
| Frontend | Backend | |
|---|---|---|
| Chạy ở đâu | Trình duyệt hoặc app của người dùng | Máy chủ, trong trung tâm dữ liệu |
| Người dùng thấy | Có — toàn bộ giao diện | Không — chỉ thấy kết quả |
| Ai kiểm soát được code | Người dùng (xem, sửa, chặn) | Chỉ nhà phát triển |
| Có tin được không | Không bao giờ | Có — nhưng phải tự kiểm tra lại |
| Ngôn ngữ phổ biến | JavaScript, TypeScript, Dart | Node.js, Python, Java, Go, C#, PHP |
| Dữ liệu ở đâu | Chỉ trong bộ nhớ tạm | Database, cache, file storage |
Dòng quan trọng nhất trong bảng trên là dòng thứ tư. Mọi thứ chạy trên máy người dùng đều có thể bị sửa. Nếu bạn kiểm tra quyền ở frontend rồi tin tưởng rằng backend sẽ không bị gọi trực tiếp — bạn đã sai. Tôi đã từng để lộ chính xác lỗi này trong dự án thật và viết hẳn một bài case study về nó (xem mục Bảo mật REST API: 14 lỗ hổng).
1.3. Ví von quán ăn
Cách dễ nhất để nhớ vai trò từng phần là hình dung một quán ăn:
Khách ngồi ở bàn → Người dùng (frontend)
Menu, ánh sáng, không khí → Giao diện
Người phục vụ nhận order → API — cầu nối giữa hai bên
Đầu bếp → Logic nghiệp vụ (backend)
Kho nguyên liệu → Database
Quản lý ca → Xác thực & phân quyền
Sổ ghi doanh thu → Log, audit trail
Khách không được phép đi thẳng vào bếp. Cũng vậy, frontend không được phép ghi thẳng vào database. Mọi yêu cầu phải qua “người phục vụ” — tức là API — và người phục vụ phải kiểm tra xem yêu cầu đó có hợp lệ không trước khi chuyển xuống bếp.
1.4. Một backend gồm những gì?
Người mới thường nghĩ backend chỉ là “code server”. Thực tế một backend hoàn chỉnh gồm nhiều lớp:
| Lớp | Vai trò | Ví dụ trong dự án thật |
|---|---|---|
| Ngôn ngữ & runtime | Chạy code | Node.js 20 + TypeScript |
| Framework | Định tuyến, middleware | Express 4.21 |
| Database | Lưu dữ liệu | PostgreSQL |
| ORM / query layer | Làm việc với database an toàn | Drizzle ORM |
| Validation | Chặn dữ liệu sai từ đầu vào | Zod |
| Auth | Chứng minh danh tính, cấp quyền | JWT + middleware phân quyền |
| Hạ tầng chạy | Vận hành 24/7 | Windows Server tự quản + Render |
| Log & monitoring | Biết hệ thống đang thế nào | Log file + endpoint /api/health |
1.5. Backend không phải là gì
Ba hiểu nhầm phổ biến:
- Backend không phải là “cơ sở dữ liệu”. Database chỉ là một phần backend dùng đến. Backend vẫn có ý nghĩa ngay cả khi chưa có database — ví dụ tính toán, kiểm tra dữ liệu, gọi dịch vụ bên ngoài.
- Backend không chỉ là CRUD. Nếu backend của bạn chỉ là thêm/sửa/xoá/xem thì bạn chưa động tới phần khó: giao dịch, phân quyền, tính nhất quán dữ liệu, xử lý lỗi.
- Backend không phải “admin panel”. Admin panel là một client khác gọi vào cùng backend đó. Nó không phải là backend.
2. Kiến trúc và nguyên lý hoạt động
Để hiểu backend, cách nhanh nhất là đi theo một request từ lúc người dùng bấm nút đến lúc nhìn thấy kết quả. Quãng đường đó có tám chặng.
2.1. Vòng đời của một request
1. Người dùng bấm "Đặt lịch"
↓
2. DNS: đổi tên miền thành địa chỉ IP
↓
3. TCP + TLS: mở kết nối, mã hoá
↓
4. Reverse proxy (Nginx / Cloudflare): định tuyến, cache, chống DDoS
↓
5. Web server / app server (Node.js + Express): nhận request
↓
6. Middleware: xác thực token → kiểm tra quyền → validate dữ liệu
↓
7. Route → Controller → Service: chạy logic nghiệp vụ
↓
8. Database / cache / dịch vụ ngoài: đọc ghi dữ liệu
↓
9. Response: trả JSON về cho client
Điểm quan trọng nằm ở chặng 6 và 7: backend phải kiểm tra trước rồi mới làm, không phải làm rồi hy vọng đúng. Toàn bộ các lỗ hổng bảo mật nghiêm trọng nhất đều là do bỏ qua một bước ở chặng 6.
2.2. Middleware — khái niệm then chốt
Middleware là các hàm chạy trước khi request tới được logic xử lý. Mỗi hàm có thể: cho đi tiếp, chặn lại, hoặc sửa request.
// Thứ tự middleware quan trọng — chạy từ trên xuống
app.use(express.json()); // 1. Đọc body JSON
app.use(requestLogger); // 2. Ghi log
app.use(rateLimit); // 3. Chặn spam
app.use(authenticate); // 4. Xác thực token
app.use("/api/admin", requireAdmin); // 5. Kiểm tra quyền
app.use("/api/bookings", bookingRoutes); // 6. Vào logic
Nếu bạn đặt requireAdmin sau route xử lý, quyền sẽ không bao giờ được kiểm tra. Đây là dạng lỗi im lặng nguy hiểm nhất trong Express: code vẫn chạy, test thủ công vẫn thấy đúng, nhưng hệ thống đang mở.
2.3. Stateless — vì sao backend không nên nhớ
Backend hiện đại được thiết kế stateless: mỗi request tự mang đủ thông tin để xử lý, server không lưu trạng thái của người dùng trong bộ nhớ.
Lý do rất thực tế: nếu server lưu phiên đăng nhập trong RAM, thì khi bạn chạy hai bản server để chịu tải, người dùng sẽ bị “đăng xuất” ngẫu nhiên tuỳ lần nào xử lý request. Token (JWT) giải quyết việc này vì thông tin nằm trong chính token do client gửi lên.
2.4. Các mô hình kiến trúc thường gặp
| Mô hình | Khi nào dùng | Đánh đổi |
|---|---|---|
| Monolith | Đội nhỏ, sản phẩm chưa rõ hình dạng, cần đi nhanh | Dễ làm, nhưng lớn dần sẽ khó tách |
| Modular monolith | Đang là monolith nhưng muốn sạch sẽ | Chi phí thấp, biên giới module rõ |
| Microservices | Nhiều đội, tải rất khác nhau giữa các phần | Rất tốn công vận hành, cần hạ tầng mạnh |
| Serverless | Tải thất thường, không muốn quản server | Rẻ khi ít tải, khó debug khi lỗi |
Với một tiệm nail hoặc một doanh nghiệp nhỏ, modular monolith là lựa chọn đúng. Chia microservices quá sớm là sai lầm tốn kém: bạn đổi một vấn đề về code lấy ba vấn đề về hạ tầng.
3. Ví dụ thực tế: hệ thống đặt lịch tiệm nail
Để backend không còn là khái niệm trừu tượng, tôi lấy chính hệ thống đang chạy của mình. Đây là app đặt lịch cho Vietnam Nails — tiệm nail tại Helsinki, Phần Lan.
3.1. Bài toán
Chủ tiệm cần một hệ thống thay thế sổ giấy và tin nhắn WhatsApp, cụ thể:
- Khách tự đặt lịch, chọn dịch vụ, chọn thợ, chọn giờ trống
- Khách nạp tiền vào ví và trả bằng ví
- Khách chat trực tiếp với chủ tiệm trong app
- Chủ tiệm quản lý lịch, chấm công, tính lương, xuất Excel
- Hệ thống gửi email nhắc hẹn tự động
- Giao diện ba ngôn ngữ: Việt, Anh, Phần Lan
Nhìn qua thì đây là “một app đặt lịch”. Nhưng mỗi gạch đầu dòng ở trên đều là một quyết định backend, và làm sai một cái là mất tiền thật.
3.2. Quy mô thật của backend
Tôi đếm số endpoint trong mã nguồn bằng cách tìm tất cả các khai báo route:
grep -cE "app\.(get|post|put|patch|delete|all)\(" server/routes.ts
# 141
141 endpoint. Con số này quan trọng vì nó tỉ lệ với số chỗ có thể bị bỏ sót. Ban đầu hệ thống chỉ có 41 chỗ kiểm tra quyền (21 dùng requireAdmin, 20 dùng requireOwner) — nghĩa là phần lớn endpoint không kiểm tra gì cả. Sau khi rà và sửa, con số thành 73 chỗ (52 + 21).
3.3. Vì sao những việc này buộc phải ở backend?
Đây là câu hỏi mà người mới học hay bỏ qua. Ta thử làm ngược lại — đặt logic ở frontend — và xem điều gì xảy ra:
| Việc | Nếu để frontend làm | Hậu quả |
|---|---|---|
| Tính giá dịch vụ | Frontend gửi giá lên cùng request đặt lịch | Khách sửa giá thành 0 € trong DevTools — mất tiền thật |
| Trừ tiền ví | Frontend tự trừ rồi báo lên | Đặt 2 lần liên tiếp cùng lúc → âm ví |
| Chặn trùng giờ | Frontend kiểm tra giờ trống | Hai khách đặt cùng slot → một người bị từ chối phút cuối |
| Phân quyền | Ẩn nút “Xoá” với người không phải admin | Gọi thẳng API bằng curl là xoá được |
Quy tắc vàng: frontend chỉ hiển thị, backend mới quyết định. Mọi kiểm tra ở frontend đều là “trải nghiệm người dùng”, không phải bảo mật.
3.4. Luồng nghiệp vụ thật: đặt lịch và trả bằng ví
Đây là luồng phức tạp nhất trong hệ thống. Nó phải làm năm việc hoặc tất cả cùng thành công, hoặc không việc nào cả:
1. Kiểm tra slot còn trống không
2. Tính giá từ database — KHÔNG lấy giá từ client
3. Tạo bản ghi lịch hẹn
4. Trừ tiền trong ví khách
5. Ghi lại giao dịch để đối soát sau này
Nếu bước 4 thất bại mà bước 3 đã ghi, khách có lịch miễn phí. Nếu bước 3 thất bại mà bước 4 đã trừ, khách mất tiền oan. Vì vậy toàn bộ năm bước phải nằm trong một transaction.
3.5. Bài học từ hệ thống thật
Ba điều tôi học được khi vận hành hệ thống này:
- Backend không có “chức năng nhỏ”. Một endpoint sửa số dư ví trông rất nhỏ, nhưng thiếu kiểm tra quyền thì bất kỳ ai cũng nạp tiền cho mình được. Đây đúng là lỗi tôi từng có.
- Lỗi backend thường im lặng. Frontend sai thì thấy ngay trên màn hình. Backend sai thì API vẫn trả 200, chỉ có dữ liệu là sai — và vài tuần sau mới phát hiện khi đối soát tiền.
- Đo mới biết mình yếu ở đâu. Tôi từng nghĩ hệ thống ổn. Đến khi viết bộ test bảo mật tử tế, nó tìm ra 14 lỗ hổng, trong đó 6 mức nghiêm trọng.
4. Code: từ ví dụ tối giản đến nghiệp vụ thật
Ta đi từ một API nhỏ nhất rồi mở rộng dần. Toàn bộ code dưới đây theo phong cách dự án thật: Express + TypeScript + PostgreSQL.
4.1. Backend nhỏ nhất có thể
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/health", (req, res) => {
res.json({ ok: true, time: new Date().toISOString() });
});
app.listen(5103, () => console.log("Backend chạy ở cổng 5103"));
Ba dòng cốt lõi: tạo app, khai báo route, lắng nghe cổng. Mọi backend đều bắt đầu như thế này và lớn lên từ đây.
4.2. Thêm database
import { Pool } from "pg";
const pool = new Pool({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
database: process.env.DB_NAME,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
max: 20, // tối đa 20 kết nối
idleTimeoutMillis: 30000, // đóng kết nối rảnh sau 30s
});
app.get("/api/services", async (req, res) => {
const { rows } = await pool.query(
"SELECT id, name, price_cents FROM services WHERE active = true ORDER BY name"
);
res.json(rows);
});
Ba chi tiết đáng chú ý:
max: 20— giới hạn kết nối. Không giới hạn thì khi có bão request, database sẽ bị sập vì hết kết nối.- Query dùng tham số, không nối chuỗi. Nối chuỗi là mở đường cho SQL Injection.
- Giá lưu bằng số nguyên (
price_cents), không dùng số thực. Số thực gây sai số làm tròn — với tiền thì không được phép.
4.3. Kiểm tra dữ liệu đầu vào
Đây là bước mà người mới hay bỏ nhất. Dữ liệu từ client luôn phải bị coi là không đáng tin:
import { z } from "zod";
const CreateBooking = z.object({
serviceId: z.string().uuid(),
staffId: z.string().uuid(),
startAt: z.string().datetime(),
paymentMethod: z.enum(["cash", "wallet"]),
});
app.post("/api/bookings", async (req, res) => {
const parsed = CreateBooking.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: "Dữ liệu không hợp lệ",
issues: parsed.error.issues.map((i) => ({
field: i.path.join("."),
message: i.message,
})),
});
}
// ... xử lý tiếp với parsed.data
});
Lưu ý safeParse chứ không phải parse. parse sẽ ném lỗi, và nếu bạn quên try/catch ở hàm async thì Express sẽ treo request mà không trả lời. Tôi từng dính đúng lỗi này: một console.error gặp ZodError làm hàm log tự ném lỗi bên trong khối catch, khiến năm endpoint treo hơn 60 giây không phản hồi. Chi tiết trong bài Bảo mật REST API.
4.4. Middleware xác thực và phân quyền
function requireAdmin(req, res, next) {
const auth = req.headers.authorization || "";
const token = auth.startsWith("Bearer ") ? auth.slice(7) : "";
if (!token) {
return res.status(401).json({ error: "Thiếu token" });
}
try {
const payload = jwt.verify(token, process.env.JWT_SECRET);
if (payload.role !== "admin") {
return res.status(403).json({ error: "Không đủ quyền" });
}
req.admin = payload;
next();
} catch {
return res.status(401).json({ error: "Token không hợp lệ" });
}
}
Ba trạng thái trả về khác nhau và phải khác nhau:
| Mã | Nghĩa | Khi nào trả |
|---|---|---|
401 |
Chưa xác thực | Không có token, hoặc token sai/hết hạn |
403 |
Đã xác thực nhưng không đủ quyền | Token hợp lệ nhưng role không phải admin |
200 |
Thành công | Token hợp lệ và đủ quyền |
Dùng 401 cho cả hai trường hợp là lỗi thiết kế: người dùng không biết nên đăng nhập lại hay xin thêm quyền.
4.5. Transaction — phần khó nhất
Đây là đoạn code thể hiện rõ nhất vì sao backend khó. Nghiệp vụ: khách đặt lịch và trả bằng ví. Năm bước phải cùng thành công hoặc cùng thất bại.
app.post("/api/bookings/pay-by-wallet", requireAuth, async (req, res) => {
const parsed = CreateBooking.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: "Dữ liệu không hợp lệ" });
}
const client = await pool.connect();
try {
await client.query("BEGIN");
// 1. Khoá dòng ví để tránh hai request cùng trừ tiền
const { rows: [wallet] } = await client.query(
"SELECT id, balance_cents FROM wallets WHERE user_id = $1 FOR UPDATE",
[req.user.id]
);
if (!wallet) {
throw new HttpError(404, "Không tìm thấy ví");
}
// 2. Tính giá từ DATABASE, tuyệt đối không lấy từ client
const { rows: [service] } = await client.query(
"SELECT price_cents FROM services WHERE id = $1 AND active = true",
[parsed.data.serviceId]
);
if (!service) {
throw new HttpError(404, "Dịch vụ không tồn tại");
}
if (wallet.balance_cents < service.price_cents) {
throw new HttpError(400, "Số dư không đủ");
}
// 3. Kiểm tra slot còn trống
const { rows: clash } = await client.query(
`SELECT 1 FROM bookings
WHERE staff_id = $1 AND status <> 'cancelled'
AND start_at = $2
FOR UPDATE`,
[parsed.data.staffId, parsed.data.startAt]
);
if (clash.length > 0) {
throw new HttpError(409, "Khung giờ đã có người đặt");
}
// 4. Tạo lịch hẹn
const { rows: [booking] } = await client.query(
`INSERT INTO bookings (user_id, service_id, staff_id, start_at, price_cents, status)
VALUES ($1, $2, $3, $4, $5, 'confirmed')
RETURNING id`,
[req.user.id, parsed.data.serviceId, parsed.data.staffId,
parsed.data.startAt, service.price_cents]
);
// 5. Trừ ví + ghi giao dịch
await client.query(
"UPDATE wallets SET balance_cents = balance_cents - $1 WHERE id = $2",
[service.price_cents, wallet.id]
);
await client.query(
`INSERT INTO wallet_transactions (wallet_id, booking_id, amount_cents, type)
VALUES ($1, $2, $3, 'payment')`,
[wallet.id, booking.id, -service.price_cents]
);
await client.query("COMMIT");
res.status(201).json({ bookingId: booking.id, charged: service.price_cents });
} catch (err) {
await client.query("ROLLBACK");
if (err instanceof HttpError) {
return res.status(err.status).json({ error: err.message });
}
logError("POST /bookings/pay-by-wallet", err);
res.status(500).json({ error: "Lỗi hệ thống" });
} finally {
client.release(); // LUÔN trả kết nối về pool
}
});
Đoạn code này chứa gần như toàn bộ kiến thức backend quan trọng:
| Kỹ thuật | Vì sao cần |
|---|---|
BEGIN / COMMIT / ROLLBACK |
Đảm bảo năm bước cùng thành công hoặc cùng huỷ |
FOR UPDATE |
Khoá dòng, chặn hai request cùng trừ một ví |
| Giá lấy từ database | Client không thể sửa giá |
| Tham số hoá query | Chống SQL Injection |
client.release() trong finally |
Rò rỉ kết nối sẽ làm sập pool sau vài giờ |
HttpError tách khỏi lỗi hệ thống |
Lỗi nghiệp vụ trả 4xx, lỗi hệ thống trả 5xx |
Cảnh báo về FOR UPDATE: nếu quên FOR UPDATE ở bước 1, hai request gửi cùng lúc có thể cùng đọc số dư 10 €, cùng thấy “đủ tiền”, rồi cùng trừ — kết quả ví thành âm. Đây gọi là race condition, và nó chỉ xuất hiện khi có tải thật. Test thủ công một mình sẽ không bao giờ thấy.
5. Năm lỗi thường gặp khi học backend
5.1. Nhồi hết logic vào controller
Controller dài 400 dòng là dấu hiệu chắc chắn của code khó bảo trì. Cách tách chuẩn:
routes.ts → khai báo đường dẫn, gắn middleware
controller.ts → đọc request, gọi service, trả response
service.ts → toàn bộ logic nghiệp vụ
repository.ts → chỉ nói chuyện với database
schema.ts → kiểu dữ liệu + validation
Lợi ích không phải là “cho đẹp”. Là để test được logic mà không cần dựng server.
5.2. Không kiểm tra dữ liệu đầu vào
Mọi endpoint nhận dữ liệu từ client đều phải validate. Không có ngoại lệ. Kể cả endpoint “nội bộ” — vì nội bộ hôm nay có thể thành công khai ngày mai.
5.3. Trả về quá nhiều dữ liệu
Đây là lỗi tôi từng mắc. Bảng users có 17 cột, và API trả về nguyên 14 trường — trong đó có hash mật khẩu. Không ai cần hash mật khẩu ở frontend. Cách sửa: dùng hàm lọc riêng.
function publicUser(row) {
return {
id: row.id,
name: row.name,
phone: row.phone,
hasPassword: Boolean(row.password), // chỉ trả TRUE/FALSE
createdAt: row.created_at,
};
}
Nguyên tắc: danh sách trắng, không phải danh sách đen. Liệt kê những gì được trả, thay vì xoá những gì không được trả. Thêm cột mới vào database sẽ không tự động lộ ra ngoài.
5.4. Nuốt lỗi
// SAI — không bao giờ làm thế này
try {
await doSomething();
} catch (e) {
console.log(e);
}
Nuốt lỗi khiến request treo hoặc trả về dữ liệu sai mà không ai biết. Express 4 không tự bắt lỗi trong hàm async — bạn phải tự xử lý.
5.5. Hardcode bí mật trong mã nguồn
Khoá JWT, mật khẩu database, API key — tất cả phải nằm trong biến môi trường. Và mã nguồn phải từ chối chạy nếu thiếu:
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 32) {
throw new Error("JWT_SECRET thiếu hoặc quá ngắn — dừng khởi động");
}
Kiểm tra kiểu “fail closed” này quan trọng hơn bạn tưởng. Nếu để mặc định, hệ thống vẫn chạy và trông bình thường — nhưng khoá đã nằm công khai trong repo.
6. Security: những thứ phải làm ngay từ đầu
Bảo mật backend không phải giai đoạn cuối. Nó là một phần của kiến trúc, và làm muộn luôn đắt hơn nhiều lần.
6.1. Sáu điểm tối thiểu
- Băm mật khẩu bằng bcrypt hoặc Argon2. Không bao giờ lưu mật khẩu gốc. Không dùng MD5/SHA1.
- Kiểm tra quyền ở backend cho mọi endpoint thay đổi dữ liệu. Danh sách trắng, không phải danh sách đen.
- Validate mọi đầu vào trước khi chạm tới database.
- Rate limit endpoint đăng nhập, gửi OTP, đổi mật khẩu.
- Bí mật trong biến môi trường, và fail closed nếu thiếu.
- Không trả trường nhạy cảm ra API, dù client có hỏi.
6.2. Kiểm tra quyền: con số biết nói
Cách kiểm tra nhanh nhất một backend có an toàn không, không cần đọc hết code:
# Đếm số endpoint
grep -cE "app\.(get|post|put|patch|delete)\(" server/routes.ts
# Đếm số chỗ kiểm tra quyền
grep -c "requireAdmin" server/routes.ts
grep -c "requireOwner" server/routes.ts
Nếu số endpoint lớn gấp nhiều lần số chỗ kiểm tra quyền, bạn có vấn đề. Trong dự án của tôi tỉ lệ ban đầu là 141 trên 41 — và kết quả đo được là 14 lỗ hổng, 6 trong đó mức nghiêm trọng.
6.3. Đọc thêm
Toàn bộ quá trình tìm và sửa 14 lỗ hổng đó được ghi lại chi tiết, kèm bằng chứng đo thật, trong bài Bảo mật REST API: 14 lỗ hổng tôi tìm thấy trong một hệ thống thật.
7. Performance: ba thứ quyết định tốc độ
90% vấn đề hiệu năng backend nằm ở ba chỗ, theo đúng thứ tự ưu tiên:
7.1. Index database
Không có index, mỗi truy vấn là một lần quét toàn bảng. Với bảng 100 dòng thì không thấy gì; với 1 triệu dòng thì API chậm từ 5 ms lên 5 giây.
-- Truy vấn hay dùng
SELECT * FROM bookings WHERE user_id = $1 ORDER BY start_at DESC;
-- Index tương ứng
CREATE INDEX idx_bookings_user_start ON bookings (user_id, start_at DESC);
Thứ tự cột trong index quan trọng: index trên (user_id, start_at) không giúp được truy vấn chỉ lọc theo start_at.
7.2. N+1 query
Lỗi kinh điển: lấy danh sách rồi lặp để lấy chi tiết. 1 truy vấn biến thành 101 truy vấn.
// SAI — N+1
const bookings = await db.query("SELECT * FROM bookings LIMIT 100");
for (const b of bookings.rows) {
b.service = (await db.query("SELECT * FROM services WHERE id = $1", [b.service_id])).rows[0];
}
// ĐÚNG — một truy vấn
const { rows } = await db.query(`
SELECT b.*, s.name AS service_name, s.price_cents
FROM bookings b
JOIN services s ON s.id = b.service_id
ORDER BY b.start_at DESC
LIMIT 100
`);
7.3. Caching và phân trang
Dữ liệu ít đổi như danh mục dịch vụ nên cache. Dữ liệu dài vô hạn bắt buộc phân trang — luôn đặt LIMIT, kể cả khi hôm nay chỉ có 20 dòng.
app.get("/api/bookings", requireAuth, async (req, res) => {
const limit = Math.min(Number(req.query.limit) || 20, 100); // chặn tối đa 100
const offset = Math.max(Number(req.query.offset) || 0, 0);
const { rows } = await pool.query(
`SELECT * FROM bookings WHERE user_id = $1
ORDER BY start_at DESC LIMIT $2 OFFSET $3`,
[req.user.id, limit, offset]
);
res.json({ data: rows, limit, offset });
});
Dòng Math.min(..., 100) rất quan trọng: không có nó, client gọi ?limit=999999 là database sập.
8. Testing: kiểm thử backend thế nào?
Backend khó test hơn frontend vì phần lớn lỗi không hiện ra trên màn hình. Ba tầng test cần thiết:
| Tầng | Kiểm tra gì | Chạy khi nào |
|---|---|---|
| Unit test | Hàm thuần: tính giá, tính lương, format ngày | Mỗi lần lưu file |
| Integration test | Endpoint + database thật | Trước mỗi lần deploy |
| Security test | Gọi API không token, token sai, sai quyền | Trước mỗi lần deploy |
Ví dụ một integration test thật cho endpoint ví:
test("nạp tiền vào ví mà không có token phải trả 401", async () => {
const res = await fetch(`${BASE}/api/users/${someUserId}/wallet`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 100 }),
});
expect(res.status).toBe(401);
});
test("nạp tiền có token admin thì số dư tăng đúng", async () => {
const before = await getBalance(someUserId);
const res = await fetch(`${BASE}/api/users/${someUserId}/wallet`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${adminToken}`,
},
body: JSON.stringify({ amount: 100 }),
});
expect(res.status).toBe(200);
const after = await getBalance(someUserId);
expect(after).toBe(before + 100);
});
Nguyên tắc quan trọng: test bảo mật phải chạy trên môi trường local, không chạy trên production. Test dạng này thay đổi dữ liệu thật.
9. Deployment: đưa backend lên chạy thật
9.1. Biến môi trường
NODE_ENV=production
PORT=5103
DATABASE_URL=postgres://user:pass@host:5432/dbname
JWT_SECRET=<chuỗi ngẫu nhiên tối thiểu 32 ký tự>
9.2. Health check — thứ đầu tiên phải có
app.get("/api/health", async (req, res) => {
try {
await pool.query("SELECT 1");
res.json({ status: "ok", db: "connected", uptime: process.uptime() });
} catch (err) {
res.status(503).json({ status: "error", db: "disconnected" });
}
});
Endpoint này trả về cả tình trạng database, không chỉ “server còn sống”. Một backend trả 200 OK trong khi database đã chết là backend đang nói dối.
9.3. Quy trình deploy an toàn
npm run build # 1. Build
# 2. Copy artifact lên server
# 3. Dừng tiến trình cũ
# 4. Chờ 5 giây cho cổng được giải phóng
# 5. Khởi động bản mới
# 6. Chờ 20 giây cho app khởi động
curl -f https://api.example.com/api/health # 7. Kiểm tra
Bước 4 và bước 7 là hai bước hay bị bỏ nhất. Bỏ bước 4 thì bản mới không bind được cổng. Bỏ bước 7 thì bạn không biết deploy thành công hay thất bại.
10. Checklist 12 điểm
Dùng danh sách này để rà lại backend đang viết:
[ ] 1. Mọi endpoint thay đổi dữ liệu đều có kiểm tra quyền
[ ] 2. Mọi đầu vào từ client đều được validate
[ ] 3. Mật khẩu được băm bằng bcrypt hoặc Argon2
[ ] 4. Không có bí mật nào trong mã nguồn
[ ] 5. Ứng dụng từ chối khởi động nếu thiếu biến môi trường
[ ] 6. Response không chứa trường nhạy cảm
[ ] 7. Mọi truy vấn dùng tham số, không nối chuỗi
[ ] 8. Các thao tác nhiều bước nằm trong transaction
[ ] 9. Có index cho các cột dùng để lọc và sắp xếp
[ ] 10. Mọi danh sách đều có phân trang
[ ] 11. Lỗi async được bắt và ghi log, không bị nuốt
[ ] 12. Có endpoint health check và log lỗi tập trung
Câu hỏi thường gặp
Học backend nên bắt đầu từ ngôn ngữ nào?
Nếu bạn đã biết JavaScript, hãy học Node.js — bạn dùng lại được kiến thức frontend và tập trung vào khái niệm backend thay vì học ngôn ngữ mới. Nếu bạn muốn đi theo hướng dữ liệu, AI hoặc làm việc với doanh nghiệp lớn, Python với FastAPI là lựa chọn tốt. Điều quan trọng hơn ngôn ngữ: bạn phải hiểu HTTP, database và bảo mật, vì ba thứ đó giống nhau ở mọi ngôn ngữ.
Backend có cần học frontend không?
Không bắt buộc, nhưng nên biết cơ bản. Bạn sẽ nói chuyện với frontend mỗi ngày, và hiểu họ cần gì giúp bạn thiết kế API tốt hơn nhiều. Ít nhất hãy biết cách gọi API bằng fetch và đọc JSON.
Backend và API có phải là một?
Không. API là giao diện của backend — cách thế giới bên ngoài nói chuyện với nó. Backend là toàn bộ phần mềm phía sau, bao gồm cả những thứ không lộ ra qua API như job chạy nền, gửi email, tính lương, sao lưu.
Có nhất thiết phải dùng framework không?
Không, nhưng nên dùng. Framework xử lý sẵn định tuyến, middleware, phân tích body, xử lý lỗi. Tự viết lại những thứ này không dạy bạn thêm được gì, chỉ tốn thời gian và tạo ra lỗi bảo mật mới.
Bao lâu thì học được backend?
Biết viết một API CRUD: khoảng 2–4 tuần. Viết được backend chạy thật, có phân quyền, transaction, xử lý lỗi và deploy được: khoảng 3–6 tháng luyện đều. Nhanh hơn nhiều nếu bạn học trên một dự án thật thay vì làm bài tập rời rạc.
Backend có cần biết Docker không?
Nên biết ở mức cơ bản. Docker giúp môi trường phát triển giống môi trường chạy thật, giải quyết câu “máy tôi chạy được mà máy chủ không chạy được”. Bạn không cần thành chuyên gia Kubernetes để làm backend.
Làm sao biết backend của mình có lỗ hổng?
Đừng đoán — hãy đo. Viết bộ test tự động gọi API bằng nhiều cách khác nhau: không có token, token sai, token của người khác, token hết hạn. Trong dự án của tôi, cách này tìm ra 14 lỗ hổng mà đọc code bằng mắt đã bỏ sót nhiều lần.
Nên học lý thuyết trước hay làm dự án trước?
Làm dự án trước, nhưng đủ nhỏ để không bị ngợp. Lý thuyết chỉ có nghĩa khi bạn đã gặp vấn đề mà nó giải quyết. Học transaction sau khi bạn đã gặp lỗi trừ tiền hai lần sẽ nhớ mãi; học trước thì ba tuần sau quên sạch.
Kết luận
Backend là phần mềm chạy trên máy chủ, chịu trách nhiệm cho mọi thứ quan trọng: logic nghiệp vụ, dữ liệu, quyền hạn và bảo mật. Người dùng không bao giờ nhìn thấy nó, nhưng mọi thứ họ tin tưởng đều do nó quyết định.
Nếu bạn đang bắt đầu, hãy nhớ bốn điều:
- Không tin client. Mọi kiểm tra ở frontend chỉ là trải nghiệm, không phải bảo mật.
- Validate ở cửa vào. Dữ liệu bẩn vào được database thì sửa rất tốn kém.
- Thao tác nhiều bước phải nằm trong transaction. Không có ngoại lệ khi có liên quan tới tiền.
- Đo, đừng đoán. Viết test, đếm endpoint, đo thời gian phản hồi. Cảm giác “chắc là ổn” đã làm tôi bỏ sót 14 lỗ hổng.
Phần còn lại là luyện tập trên dự án thật. Lý thuyết backend không khó; cái khó là giữ được sự tỉ mỉ khi hệ thống đã chạy và không ai còn nhớ vì sao đoạn code kia được viết như vậy.
Bài tiếp theo trong loạt: REST API là gì? Hướng dẫn xây dựng REST API từ A-Z — đi sâu vào cách thiết kế endpoint, mã trạng thái, versioning và tài liệu API.
Bài liên quan:
- Bảo mật REST API: 14 lỗ hổng tôi tìm thấy trong một hệ thống thật và cách sửa hết
- Backend tiệm nail: Express + PostgreSQL + 118 API (case study thực tế)
- Học backend từ dự án thực tế — Khóa NodeJS & FastAPI
Học backend bằng cách làm dự án thật
Đọc về transaction hay middleware sẽ không giúp bạn nhớ lâu. Cái giúp nhớ là tự tay làm hỏng một lần rồi sửa.
Tại Hướng Nghiệp Dữ Liệu, các khoá học backend được dạy trực tiếp trên dự án đang chạy — cùng loại hệ thống mà bạn vừa đọc ở trên. Bạn sẽ tự viết API, tự gặp lỗi trừ tiền hai lần, tự tìm ra lỗ hổng phân quyền, và tự sửa.
- Khoá Lập trình NodeJS — Express, PostgreSQL, REST API, xác thực, deploy
- Khoá Python FastAPI — backend Python, Pydantic, SQLAlchemy, API cho AI
Xem thêm các bài khác trong loạt Backend tại huongnghiepdulieu.com.
Đặng Trí Thanh
Giám đốc Công nghệ · DNT Digital · Giảng viên HNDL Hướng Nghiệp Dữ LiệuĐặng Trí Thanh — Founder & CTO · Hướng Nghiệp Dữ Liệu - DNT Digital. Chuyên đào tạo và triển khai thực chiến Python, MT5 và hệ thống bot auto trading / IB cho học viên và doanh nghiệp.