Backend với AI
Xây dựng REST API production-ready với Express.js, database integration và authentication — tận dụng AI để viết code an toàn và chuẩn cấu trúc.
🎯 Mục tiêu học tập
- Xây dựng Express.js API với cấu trúc MVC chuyên nghiệp
- Integrate Prisma ORM với SQLite database
- Implement JWT authentication an toàn
- Dùng AI để review security và generate boilerplate nhanh
- Áp dụng OWASP Top 10 — Helmet, CORS whitelist, Zod validation, JWT refresh tokens, rate limiting
Bài 7.1 — Express.js REST API
Quy Trình Thiết Kế API Trước Khi Code
List tất cả "thực thể" trong app: User, Post, Comment, Order... Mỗi resource = 1 nhóm endpoints. Đây là nền tảng API design.
Mỗi resource có 5 operations chuẩn: list, create, get-one, update, delete. URL dạng /api/posts và /api/posts/:id.
Từ resources, define Prisma schema: models, fields, relationships. Hỏi Copilot: "Thiết kế Prisma schema cho [list resources]".
Document request body và response format của từng endpoint. Đây là "hợp đồng" với frontend — làm rõ trước khi code.
Mỗi endpoint = 1 prompt rõ ràng. Bắt đầu từ CRUD đơn giản nhất. Test ngay với REST Client sau khi code xong.
Thêm authentication, validation, rate limiting sau khi CRUD hoạt động. Đừng add security khi chưa có business logic.
Cấu trúc project Express chuẩn MVC
backend/
├── src/
│ ├── routes/ # Route definitions
│ │ ├── index.js # Mount all routes
│ │ ├── auth.js
│ │ └── users.js
│ ├── controllers/ # Business logic
│ │ ├── authController.js
│ │ └── userController.js
│ ├── middleware/ # Custom middleware
│ │ ├── auth.js # JWT verification
│ │ ├── validate.js# Request validation
│ │ └── errorHandler.js
│ ├── models/ # Database models (Prisma)
│ ├── utils/ # Helper functions
│ └── app.js # Express app setup
├── prisma/
│ └── schema.prisma # Database schema
├── .env
├── .env.example
└── package.json
Express App Setup hoàn chỉnh
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const routes = require('./routes');
const errorHandler = require('./middleware/errorHandler');
const app = express();
// ---- SECURITY MIDDLEWARE ----
app.use(helmet()); // Set security-related HTTP headers
// CORS — chỉ cho phép frontend của bạn
app.use(cors({
origin: process.env.ALLOWED_ORIGIN || 'http://localhost:5173',
credentials: true,
}));
// Rate limiting — prevent brute force attacks
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // 100 requests per window per IP
standardHeaders: true,
legacyHeaders: false,
});
app.use('/api/', limiter);
// ---- PARSING MIDDLEWARE ----
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));
// ---- ROUTES ----
app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date() }));
app.use('/api', routes);
// ---- ERROR HANDLER (phải đặt sau cùng) ----
app.use(errorHandler);
module.exports = app;
/**
* Centralized Error Handler
* Mọi lỗi đều được xử lý tại đây
*/
function errorHandler(err, req, res, next) {
console.error(err.stack);
// Prisma errors
if (err.code === 'P2002') {
return res.status(409).json({ error: 'Dữ liệu đã tồn tại (duplicate)' });
}
if (err.code === 'P2025') {
return res.status(404).json({ error: 'Không tìm thấy dữ liệu' });
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
return res.status(401).json({ error: 'Token không hợp lệ' });
}
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token đã hết hạn' });
}
// Custom errors (throw new Error với status)
if (err.statusCode) {
return res.status(err.statusCode).json({ error: err.message });
}
// Default server error (KHÔNG expose stack trace ra ngoài)
res.status(500).json({
error: process.env.NODE_ENV === 'production'
? 'Đã xảy ra lỗi server'
: err.message,
});
}
module.exports = errorHandler;
Bài 7.2 — Database với Prisma ORM
| HTTP Method | Hành động CRUD | Endpoint mẫu | Response code |
|---|---|---|---|
GET | Read (lấy dữ liệu) | GET /api/posts | 200 OK |
POST | Create (tạo mới) | POST /api/posts | 201 Created |
PUT | Replace (thay toàn bộ) | PUT /api/posts/:id | 200 OK |
PATCH | Update (sửa một phần) | PATCH /api/posts/:id | 200 OK |
DELETE | Delete (xóa) | DELETE /api/posts/:id | 204 No Content |
| HTTP Status Codes quan trọng | |||
| 200 OK | Thành công | 201 Created | Tạo mới thành công |
| 400 Bad Request | Input không hợp lệ | 401 Unauthorized | Chưa xác thực |
| 403 Forbidden | Không có quyền | 404 Not Found | Không tìm thấy |
| 409 Conflict | Dữ liệu đã tồn tại | 429 Too Many Requests | Rate limit exceeded |
| 500 Internal Server Error | Lỗi server | 503 Service Unavailable | Server đang bảo trì |
Prisma là ORM hiện đại nhất cho Node.js — type-safe, auto-completion tuyệt vời, và AI biết Prisma rất tốt.
npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
username String @unique
password String // Bcrypt hash
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
authorId Int
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
tags Tag[] @relation("PostTags")
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[] @relation("PostTags")
}
# Tạo migration và apply vào database
npx prisma migrate dev --name init
# Generate Prisma Client (type-safe queries)
npx prisma generate
# Mở Prisma Studio — GUI xem database
npx prisma studio
CRUD Operations với Prisma
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// ---- CREATE ----
async function createPost(req, res, next) {
try {
const { title, content, tags } = req.body;
const authorId = req.user.id; // Set bởi auth middleware
const post = await prisma.post.create({
data: {
title,
content,
authorId,
tags: {
connectOrCreate: tags?.map(name => ({
where: { name },
create: { name },
})) ?? [],
},
},
include: { author: { select: { id: true, username: true } }, tags: true },
});
res.status(201).json(post);
} catch (err) {
next(err); // Delegate to error handler
}
}
// ---- READ (with pagination) ----
async function getPosts(req, res, next) {
try {
const page = Math.max(1, parseInt(req.query.page) || 1);
const limit = Math.min(50, parseInt(req.query.limit) || 10);
const skip = (page - 1) * limit;
const [posts, total] = await Promise.all([
prisma.post.findMany({
where: { published: true },
skip,
take: limit,
orderBy: { createdAt: 'desc' },
include: {
author: { select: { id: true, username: true } },
tags: true,
},
}),
prisma.post.count({ where: { published: true } }),
]);
res.json({
data: posts,
pagination: { page, limit, total, pages: Math.ceil(total / limit) },
});
} catch (err) {
next(err);
}
}
// ---- UPDATE ----
async function updatePost(req, res, next) {
try {
const id = parseInt(req.params.id);
// Kiểm tra quyền: chỉ author mới được sửa
const existing = await prisma.post.findUnique({ where: { id } });
if (!existing) return res.status(404).json({ error: 'Post không tồn tại' });
if (existing.authorId !== req.user.id) {
return res.status(403).json({ error: 'Không có quyền chỉnh sửa bài này' });
}
const post = await prisma.post.update({
where: { id },
data: req.body,
});
res.json(post);
} catch (err) {
next(err);
}
}
// ---- DELETE ----
async function deletePost(req, res, next) {
try {
const id = parseInt(req.params.id);
await prisma.post.delete({ where: { id } });
res.status(204).send();
} catch (err) {
next(err);
}
}
module.exports = { createPost, getPosts, updatePost, deletePost };
Bài 7.3 — Authentication với JWT
Authentication là phần code nhạy cảm nhất. Khi dùng AI generate auth code: 1) Review kỹ từng dòng, 2) Không dùng hardcoded secrets, 3) Luôn hash password với bcrypt, 4) Validate JWT trên server không chỉ check format.
npm install bcryptjs jsonwebtoken zod
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { z } = require('zod');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// Validation schemas (Zod)
const registerSchema = z.object({
email: z.string().email('Email không hợp lệ'),
username: z.string().min(3).max(30).regex(/^[a-z0-9_]+$/, 'Username chỉ gồm chữ thường, số, _'),
password: z.string().min(8, 'Mật khẩu ít nhất 8 ký tự')
.regex(/[A-Z]/, 'Cần ít nhất 1 chữ hoa')
.regex(/[0-9]/, 'Cần ít nhất 1 số'),
});
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(1),
});
// ---- REGISTER ----
async function register(req, res, next) {
try {
// Validate input
const parsed = registerSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ errors: parsed.error.flatten().fieldErrors });
}
const { email, username, password } = parsed.data;
// Check duplicate
const exists = await prisma.user.findFirst({
where: { OR: [{ email }, { username }] },
});
if (exists) {
return res.status(409).json({ error: 'Email hoặc username đã được sử dụng' });
}
// Hash password — NEVER store plain text
const hashedPassword = await bcrypt.hash(password, 12); // 12 rounds
const user = await prisma.user.create({
data: { email, username, password: hashedPassword },
select: { id: true, email: true, username: true, createdAt: true }, // Exclude password
});
const token = generateToken(user.id);
res.status(201).json({ user, token });
} catch (err) {
next(err);
}
}
// ---- LOGIN ----
async function login(req, res, next) {
try {
const parsed = loginSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({ error: 'Email và mật khẩu không được để trống' });
}
const { email, password } = parsed.data;
const user = await prisma.user.findUnique({ where: { email } });
// Constant-time comparison — prevent timing attacks
const passwordMatch = user
? await bcrypt.compare(password, user.password)
: await bcrypt.compare(password, '$2a$12$invalid_hash_for_timing_protection');
if (!user || !passwordMatch) {
// Trả về cùng message — không reveal xem email có tồn tại không
return res.status(401).json({ error: 'Email hoặc mật khẩu không đúng' });
}
const token = generateToken(user.id);
res.json({
user: { id: user.id, email: user.email, username: user.username },
token,
});
} catch (err) {
next(err);
}
}
function generateToken(userId) {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
}
module.exports = { register, login };
const jwt = require('jsonwebtoken');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function requireAuth(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Token không được cung cấp' });
}
const token = authHeader.slice(7); // Remove "Bearer "
const decoded = jwt.verify(token, process.env.JWT_SECRET);
// Verify user vẫn tồn tại trong DB
const user = await prisma.user.findUnique({
where: { id: decoded.userId },
select: { id: true, email: true, username: true },
});
if (!user) {
return res.status(401).json({ error: 'Người dùng không tồn tại' });
}
req.user = user; // Attach user to request
next();
} catch (err) {
next(err); // JWT errors handled by errorHandler
}
}
module.exports = { requireAuth };
Test API với REST Client Extension
@base = http://localhost:3000/api
@token = your_jwt_token_here
### Đăng ký
POST {{base}}/auth/register
Content-Type: application/json
{
"email": "test@example.com",
"username": "testuser",
"password": "Password123"
}
### Đăng nhập
POST {{base}}/auth/login
Content-Type: application/json
{
"email": "test@example.com",
"password": "Password123"
}
### Tạo post (cần auth)
POST {{base}}/posts
Content-Type: application/json
Authorization: Bearer {{token}}
{
"title": "Bài viết test",
"content": "Nội dung bài viết",
"tags": ["nodejs", "ai"]
}
Bài 7.4 — Dự Án Thực Hành: Xây Dựng REST API Ghi Chú Với Express + Prisma + JWT
Chúng ta sẽ xây dựng một REST API đầy đủ cho ứng dụng ghi chú — bao gồm xác thực người dùng với JWT, CRUD cho ghi chú, middleware, và validation. Đây là backend pattern chuẩn mà bạn sẽ dùng trong hầu hết dự án thực tế.
Bước 1 — Khởi Tạo Dự Án Express
# Tạo thư mục project
mkdir notes-api
cd notes-api
npm init -y
# Cài dependencies chính
npm install express bcryptjs jsonwebtoken dotenv cors helmet express-validator
# Cài Prisma ORM
npm install prisma @prisma/client
npx prisma init
# Cài dev dependencies
npm install --save-dev nodemon
# Thêm script vào package.json (thêm thủ công hoặc dùng Copilot)
# "dev": "nodemon src/index.js"
mkdir src src/routes src/middleware src/controllers
# Windows:
ni src/index.js, src/routes/auth.js, src/routes/notes.js, src/middleware/auth.js, src/middleware/validate.js, src/controllers/authController.js, src/controllers/notesController.js
# macOS/Linux:
touch src/index.js src/routes/auth.js src/routes/notes.js src/middleware/auth.js src/middleware/validate.js src/controllers/authController.js src/controllers/notesController.js
Bước 2 — Cấu Hình Prisma Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite" // Dùng SQLite để đơn giản, đổi thành "postgresql" khi production
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String @unique
password String // Lưu hash, KHÔNG lưu plaintext
name String?
createdAt DateTime @default(now())
notes Note[] // Relation: 1 user có nhiều notes
}
model Note {
id Int @id @default(autoincrement())
title String
content String
pinned Boolean @default(false)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
# Tạo file .env
echo 'DATABASE_URL="file:./dev.db"' > .env
echo 'JWT_SECRET="your-super-secret-key-change-in-production-min-32-chars"' >> .env
echo 'PORT=3000' >> .env
# Chạy migration lần đầu
npx prisma migrate dev --name init
# Lệnh này tạo file dev.db và bảng trong database
# Xem database trong Prisma Studio (GUI)
npx prisma studio
# Mở trình duyệt tại: http://localhost:5555
Bước 3 — Viết Middleware
// src/middleware/auth.js — Xác thực JWT token
const jwt = require('jsonwebtoken');
/**
* Middleware kiểm tra Bearer token trong header Authorization
* Nếu hợp lệ: thêm req.user, gọi next()
* Nếu không hợp lệ: trả về 401
*/
function requireAuth(req, res, next) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'Yêu cầu đăng nhập để tiếp tục' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = { id: decoded.userId, email: decoded.email };
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Phiên đăng nhập đã hết hạn, vui lòng đăng nhập lại' });
}
return res.status(401).json({ error: 'Token không hợp lệ' });
}
}
module.exports = { requireAuth };
Bước 4 — Viết Auth Controller
// src/controllers/authController.js
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const SALT_ROUNDS = 12; // Số vòng hash bcrypt — 12 là khuyến nghị năm 2024
/**
* POST /api/auth/register
* Body: { email, password, name? }
*/
async function register(req, res) {
const { email, password, name } = req.body;
try {
// Kiểm tra email đã tồn tại chưa
const existing = await prisma.user.findUnique({ where: { email } });
if (existing) {
return res.status(409).json({ error: 'Email này đã được đăng ký' });
}
// Hash password trước khi lưu — KHÔNG BAO GIỜ lưu plaintext
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
const user = await prisma.user.create({
data: { email, password: hashedPassword, name },
select: { id: true, email: true, name: true, createdAt: true } // Không trả về password
});
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.status(201).json({ message: 'Đăng ký thành công', user, token });
} catch (err) {
console.error('Register error:', err);
res.status(500).json({ error: 'Lỗi server, vui lòng thử lại' });
}
}
/**
* POST /api/auth/login
* Body: { email, password }
*/
async function login(req, res) {
const { email, password } = req.body;
try {
const user = await prisma.user.findUnique({ where: { email } });
// Dùng cùng error message cho cả 2 trường hợp để tránh user enumeration attack
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Email hoặc mật khẩu không đúng' });
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '7d' }
);
res.json({
message: 'Đăng nhập thành công',
user: { id: user.id, email: user.email, name: user.name },
token
});
} catch (err) {
console.error('Login error:', err);
res.status(500).json({ error: 'Lỗi server' });
}
}
module.exports = { register, login };
Bước 5 — Notes Controller Và Routes
// src/controllers/notesController.js
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
// GET /api/notes — Lấy tất cả ghi chú của user đang đăng nhập
async function getNotes(req, res) {
try {
const notes = await prisma.note.findMany({
where: { userId: req.user.id },
orderBy: [{ pinned: 'desc' }, { updatedAt: 'desc' }]
});
res.json({ notes, total: notes.length });
} catch (err) {
res.status(500).json({ error: 'Lỗi khi lấy ghi chú' });
}
}
// POST /api/notes — Tạo ghi chú mới
async function createNote(req, res) {
const { title, content, pinned = false } = req.body;
try {
const note = await prisma.note.create({
data: { title, content, pinned, userId: req.user.id }
});
res.status(201).json({ message: 'Đã tạo ghi chú', note });
} catch (err) {
res.status(500).json({ error: 'Lỗi khi tạo ghi chú' });
}
}
// PUT /api/notes/:id — Cập nhật ghi chú
async function updateNote(req, res) {
const noteId = parseInt(req.params.id);
const { title, content, pinned } = req.body;
try {
// Kiểm tra note thuộc về user này không
const existing = await prisma.note.findFirst({ where: { id: noteId, userId: req.user.id } });
if (!existing) return res.status(404).json({ error: 'Ghi chú không tồn tại' });
const note = await prisma.note.update({
where: { id: noteId },
data: { ...(title && { title }), ...(content !== undefined && { content }), ...(pinned !== undefined && { pinned }) }
});
res.json({ message: 'Đã cập nhật', note });
} catch (err) {
res.status(500).json({ error: 'Lỗi khi cập nhật' });
}
}
// DELETE /api/notes/:id
async function deleteNote(req, res) {
const noteId = parseInt(req.params.id);
try {
const existing = await prisma.note.findFirst({ where: { id: noteId, userId: req.user.id } });
if (!existing) return res.status(404).json({ error: 'Ghi chú không tồn tại' });
await prisma.note.delete({ where: { id: noteId } });
res.json({ message: 'Đã xóa ghi chú' });
} catch (err) {
res.status(500).json({ error: 'Lỗi khi xóa' });
}
}
module.exports = { getNotes, createNote, updateNote, deleteNote };
// src/index.js
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const authRoutes = require('./routes/auth');
const notesRoutes = require('./routes/notes');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware bảo mật và parsing
app.use(helmet()); // Tự động thêm các HTTP security headers
app.use(cors()); // Cho phép cross-origin requests (cần cấu hình kỹ ở production)
app.use(express.json()); // Parse JSON request body
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/notes', notesRoutes);
// Health check endpoint
app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date() }));
// 404 handler
app.use((req, res) => res.status(404).json({ error: 'Endpoint không tồn tại' }));
// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Lỗi server nội bộ' });
});
app.listen(PORT, () => {
console.log(`🚀 API server đang chạy tại http://localhost:${PORT}`);
});
Bước 6 — Test API Với curl
# Khởi động server
npm run dev
# === TEST CÁC ENDPOINTS ===
# 1. Đăng ký tài khoản mới
curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Password123","name":"Nguyen Van A"}'
# 2. Đăng nhập (lưu token từ response)
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"Password123"}'
# Lưu token vào biến shell (copy từ response)
TOKEN="eyJhbGci..."
# 3. Tạo ghi chú mới (cần token)
curl -X POST http://localhost:3000/api/notes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"Ghi chú đầu tiên","content":"Nội dung ghi chú...","pinned":true}'
# 4. Lấy tất cả ghi chú
curl -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/notes
# 5. Cập nhật ghi chú (thay 1 bằng ID thực tế)
curl -X PUT http://localhost:3000/api/notes/1 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"title":"Tiêu đề đã sửa"}'
# 6. Xóa ghi chú
curl -X DELETE http://localhost:3000/api/notes/1 \
-H "Authorization: Bearer $TOKEN"
Bạn vừa xây dựng xong một REST API production-ready với: JWT authentication, bcrypt password hashing, Prisma ORM, CORS + Helmet security headers, và proper error handling. Đây là nền tảng của mọi ứng dụng web hiện đại.
- Khi AI generate validation code, luôn hỏi thêm: "Liệt kê các trường hợp bypass validation mà attacker có thể dùng." AI thường tìm ra được 2-3 lỗ hổng mà mình bỏ qua.
- Đặt tất cả database queries trong try/catch và delegate đến error handler. Không bao giờ expose Prisma error message trực tiếp ra client.
- JWT secret nên có độ dài ít nhất 32 ký tự random. Dùng:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"để generate. - Thêm request logging (morgan) ngay từ đầu — khi có bug production, logs là thứ duy nhất bạn có để debug.
Bài 7.5 — Security, Rate Limiting & API Best Practices
API security không phải tùy chọn — đây là yêu cầu bắt buộc. Trong bài này bạn sẽ học những pattern bảo mật quan trọng nhất và cách dùng AI để review security toàn diện.
- Helmet: security HTTP headers
- Rate limiting: chống brute force
- Input validation + sanitization
- SQL injection prevention (Prisma)
- CORS đúng cấu hình
- Versioning: /api/v1/
- Consistent response format
- Pagination chuẩn (cursor/offset)
- HTTP status codes đúng
- API documentation (Swagger)
Security Setup Hoàn Chỉnh
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import { body, validationResult } from 'express-validator';
export function setupSecurity(app: express.Application) {
// 1. Helmet — security headers (XSS, clickjacking, sniffing, etc.)
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
}
}
}));
// 2. CORS — chỉ cho phép origins cụ thể
const allowedOrigins = (process.env.ALLOWED_ORIGINS || '').split(',');
app.use(cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
}));
// 3. General rate limit — 100 requests/15 min
app.use(rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: 'Too many requests, please try again later.' },
standardHeaders: true,
legacyHeaders: false,
}));
}
// 4. Stricter rate limit cho auth endpoints
export const authRateLimit = rateLimit({
windowMs: 15 * 60 * 1000,
max: 5, // max 5 login attempts / 15 min
message: { error: 'Too many login attempts.' },
skipSuccessfulRequests: true,
});
// 5. Validation middleware với express-validator
export const validateRegister = [
body('email').isEmail().normalizeEmail().withMessage('Email không hợp lệ'),
body('password')
.isLength({ min: 8 }).withMessage('Password tối thiểu 8 ký tự')
.matches(/[A-Z]/).withMessage('Phải có ít nhất 1 chữ hoa')
.matches(/[0-9]/).withMessage('Phải có ít nhất 1 số'),
body('name').trim().isLength({ min: 2, max: 50 }).withMessage('Tên 2-50 ký tự'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({ errors: errors.array() });
}
next();
}
];
- A01 Broken Access Control: User A có thể đọc/sửa data của User B → luôn check ownership
- A03 Injection: SQL injection, NoSQL injection → dùng Prisma ORM, không raw query
- A07 Auth Failures: Weak passwords, no rate limit → bcrypt + rate limiting
- A09 Security Logging: Không log failed auth attempts → dùng morgan + winston
Consistent Response Format
// Chuẩn hóa response format cho toàn bộ API
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
meta?: {
total?: number;
page?: number;
limit?: number;
totalPages?: number;
};
}
export function successResponse<T>(
res: Response, data: T, message = 'Success', statusCode = 200, meta?: object
) {
return res.status(statusCode).json({
success: true, data, message, ...(meta ? { meta } : {})
} as ApiResponse<T>);
}
export function errorResponse(
res: Response, error: string, statusCode = 400, details?: unknown
) {
return res.status(statusCode).json({
success: false, error,
...(process.env.NODE_ENV === 'development' && details ? { details } : {})
} as ApiResponse<never>);
}
// Usage trong controller:
// return successResponse(res, user, 'User created', 201);
// return errorResponse(res, 'Email already exists', 409);
Hãy review toàn bộ API code sau theo góc độ security (OWASP Top 10): [paste Express routes/controllers code vào đây] Kiểm tra: 1. Broken Access Control: có endpoint nào thiếu auth check không? 2. Injection vulnerabilities: raw SQL queries, NoSQL injection risks? 3. Authentication issues: JWT implementation có đúng không? 4. Sensitive data exposure: response có trả về data nhạy cảm không? 5. Rate limiting: endpoints nào cần rate limit chặt hơn? 6. Input validation: tất cả user inputs có được validate không? Với mỗi lỗ hổng tìm thấy: giải thích risk + code fix cụ thể.
API Documentation với Swagger/OpenAPI
npm install swagger-jsdoc swagger-ui-express
npm install -D @types/swagger-jsdoc @types/swagger-ui-express
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import express from 'express';
const options = {
definition: {
openapi: '3.0.0',
info: { title: 'My API', version: '1.0.0', description: 'REST API docs' },
components: {
securitySchemes: {
bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' }
}
}
},
apis: ['./src/routes/*.ts'], // scan JSDoc comments trong routes
};
export function setupDocs(app: express.Application) {
const specs = swaggerJsdoc(options);
app.use('/api/docs', swaggerUi.serve, swaggerUi.setup(specs));
// Truy cập: http://localhost:3000/api/docs
}
// Trong routes, dùng JSDoc comment để generate docs:
/**
* @swagger
* /api/users:
* get:
* summary: Get all users (paginated)
* security: [{ bearerAuth: [] }]
* parameters:
* - in: query
* name: page
* schema: { type: integer, default: 1 }
* - in: query
* name: limit
* schema: { type: integer, default: 10, maximum: 100 }
* responses:
* 200:
* description: List of users
*/
- Thêm Helmet + CORS + Rate Limiting vào Express app của bạn
- Hỏi Copilot: "Review auth endpoints của tôi theo OWASP Top 10"
- Thêm express-validator cho tất cả POST/PUT endpoints
- Implement consistent response format với helper functions
- Setup Swagger docs cho ít nhất 5 endpoints
- Kiểm tra:
curl -s http://localhost:3000/api/docs→ thấy Swagger UI
- Helmet headers xuất hiện trong response: X-Content-Type-Options, X-Frame-Options
- Rate limit hoạt động: sau 5 login sai → nhận 429 Too Many Requests
- Tất cả routes có auth middleware (trừ login/register)
- Swagger UI accessible tại /api/docs
- Response format nhất quán:
{"success": true, "data": {...}}
Bài 7.6 — Bảo Mật API: OWASP Top 10 Thực Chiến
OWASP Top 10 là danh sách 10 lỗ hổng bảo mật nguy hiểm nhất cho web application — được cập nhật mỗi 3–4 năm bởi cộng đồng bảo mật toàn cầu. Biết cách phòng chống từng lỗ hổng là yêu cầu tối thiểu của mọi backend developer chuyên nghiệp.
Theo IBM Cost of Data Breach 2024: trung bình một vụ vi phạm dữ liệu tốn $4.88 triệu USD. 90% các cuộc tấn công khai thác lỗ hổng trong OWASP Top 10. Phần lớn có thể phòng tránh bằng code practice đúng đắn.
npm audit thường xuyên. Cập nhật dependencies. Dùng npm audit fix.Triển Khai Bảo Mật: Code Thực Tế
1. Helmet.js — Security Headers
import helmet from 'helmet';
import express from 'express';
const app = express();
// Helmet tự động thêm 12+ HTTP security headers
app.use(helmet({
// Content Security Policy — ngăn XSS bằng cách kiểm soát nguồn script
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // Chỉ script từ cùng domain
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
// HSTS — bắt buộc HTTPS trong 1 năm
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
// Ẩn thông tin server
hidePoweredBy: true,
// Ngăn clickjacking
frameguard: { action: 'deny' },
// Ngăn MIME type sniffing
noSniff: true,
}));
2. CORS — Whitelist Origins
import cors from 'cors';
const allowedOrigins = [
'https://myapp.com',
'https://www.myapp.com',
// Thêm cho dev local:
...(process.env.NODE_ENV === 'development' ? ['http://localhost:5173'] : []),
];
app.use(cors({
origin: (origin, callback) => {
// Cho phép requests không có origin (Postman, curl, server-to-server)
if (!origin) return callback(null, true);
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error(`CORS: Origin ${origin} not allowed`));
}
},
credentials: true, // Cho phép gửi cookies
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Cache preflight 24h
}));
3. Input Validation với Zod
import { z } from 'zod';
import { Request, Response, NextFunction } from 'express';
// Schema định nghĩa rõ ràng — làm documentation luôn
const RegisterSchema = z.object({
email: z.string().email('Email không hợp lệ').toLowerCase().trim(),
password: z
.string()
.min(8, 'Mật khẩu tối thiểu 8 ký tự')
.regex(/[A-Z]/, 'Cần ít nhất 1 chữ hoa')
.regex(/[0-9]/, 'Cần ít nhất 1 số')
.regex(/[^A-Za-z0-9]/, 'Cần ít nhất 1 ký tự đặc biệt'),
name: z.string().min(2).max(100).trim(),
role: z.enum(['user', 'admin']).default('user'),
});
// Middleware factory — reusable cho mọi route
function validate(schema: z.ZodSchema) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
success: false,
errors: result.error.errors.map(e => ({
field: e.path.join('.'),
message: e.message,
})),
});
}
req.body = result.data; // Dùng data đã được sanitize
next();
};
}
// Dùng trong route:
router.post('/register', validate(RegisterSchema), registerController);
4. SQL Injection Prevention
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// ❌ NGUY HIỂM — String concatenation trong raw query
const dangerous = await prisma.$queryRawUnsafe(
`SELECT * FROM users WHERE email = '${userInput}'`
// Nếu userInput = "' OR '1'='1" → lấy được toàn bộ users!
);
// ✅ AN TOÀN — Prisma ORM tự động parameterize
const safe = await prisma.user.findUnique({
where: { email: userInput }, // Prisma escape automatically
});
// ✅ AN TOÀN — Raw query với template literal (tagged)
const safeRaw = await prisma.$queryRaw`
SELECT * FROM users WHERE email = ${userInput}
`; // Prisma dùng prepared statements, input KHÔNG được interpolate trực tiếp
// ✅ AN TOÀN — Parameterized nếu cần raw SQL
import { Prisma } from '@prisma/client';
const safeParam = await prisma.$queryRaw(
Prisma.sql`SELECT * FROM users WHERE email = ${userInput}`
);
5. JWT Best Practices
import jwt from 'jsonwebtoken';
import crypto from 'crypto';
// ✅ Access Token: ngắn hạn (15 phút)
function signAccessToken(userId: string): string {
return jwt.sign(
{ sub: userId, type: 'access' },
process.env.JWT_ACCESS_SECRET!, // Min 32 ký tự ngẫu nhiên
{ expiresIn: '15m', algorithm: 'HS256' }
);
}
// ✅ Refresh Token: dài hạn (7 ngày), lưu trong DB để revoke được
function signRefreshToken(userId: string): string {
const token = crypto.randomBytes(64).toString('hex');
// Lưu token hash vào DB (không lưu raw token):
// await prisma.refreshToken.create({ data: { userId, tokenHash: hash(token), expiresAt: ... }})
return token;
}
// ✅ Verify middleware
function verifyAccessToken(token: string) {
try {
const payload = jwt.verify(token, process.env.JWT_ACCESS_SECRET!) as jwt.JwtPayload;
if (payload.type !== 'access') throw new Error('Invalid token type');
return payload;
} catch (err) {
if (err instanceof jwt.TokenExpiredError) {
throw new AppError('Token hết hạn, vui lòng đăng nhập lại', 401);
}
throw new AppError('Token không hợp lệ', 401);
}
}
// ✅ Logout: invalidate refresh token trong DB
async function logout(refreshToken: string) {
await prisma.refreshToken.deleteMany({
where: { tokenHash: hash(refreshToken) }
});
}
6. Rate Limiting Nâng Cao
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis'; // Production: dùng Redis store
// Global rate limit — áp dụng cho tất cả routes
export const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 phút
max: 500, // 500 requests/IP/window
standardHeaders: 'draft-7',
legacyHeaders: false,
message: { success: false, message: 'Quá nhiều request, vui lòng thử lại sau.' },
});
// Strict limit cho auth endpoints — phòng brute-force
export const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 10, // Chỉ 10 lần login/IP/15 phút
skipSuccessfulRequests: true, // Không tính request thành công
message: { success: false, message: 'Quá nhiều lần thử đăng nhập. Vui lòng đợi 15 phút.' },
});
// Áp dụng:
app.use(globalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/forgot-password', authLimiter);
Paste code vào Copilot Chat và hỏi: "Review đoạn code này theo OWASP Top 10. Liệt kê mọi lỗ hổng bảo mật tìm thấy và cách sửa." AI phát hiện được 70–80% lỗi phổ biến và giải thích rõ hơn hầu hết tài liệu kỹ thuật.
- [ ]
NODE_ENV=production— tắt stack trace trong error response - [ ] Tất cả secrets trong
.env— không hardcode, không commit - [ ]
npm audit— 0 high/critical vulnerabilities - [ ] HTTPS bắt buộc — redirect HTTP sang HTTPS
- [ ] Helmet + CORS + Rate Limit đều active
- [ ] JWT secret tối thiểu 32 ký tự ngẫu nhiên
- [ ] Password hash với bcrypt rounds ≥ 12
- [ ] Input validation trên MỌI endpoint nhận user input
- [ ] Authorization check — user chỉ truy cập data của mình
- [ ] Logs không chứa sensitive data (passwords, tokens, PII)
- Setup Express + TypeScript + Prisma với cấu trúc MVC hoàn chỉnh
- Implement auth: register + login + JWT middleware + /me endpoint
- CRUD hoàn chỉnh cho 1 resource với authorization check (chỉ owner mới được sửa)
- Thêm security: Helmet + CORS + Rate Limiting + Validation
- Hỏi Copilot Chat: "Review toàn bộ auth flow của tôi, tìm security issues"
- Setup Swagger docs và test tất cả endpoints qua UI
🗒 Tóm Tắt Chương 7
- Express app structure: routes → controllers → middleware, tách biệt concerns
- Helmet + CORS + Rate Limiting — 3 middleware bảo mật bắt buộc ngay từ đầu
- Prisma ORM: schema → migrate → generate → type-safe queries, chống SQL injection
- Password: bcrypt hash 12 rounds, KHÔNG lưu plain text, KHÔNG expose trong response
- JWT: sign với secret mạnh từ .env, verify trong middleware, handle expiry gracefully
- express-validator: validate input tại boundary, trả về errors rõ ràng cho frontend
- OWASP Top 10: AI review code security — bắt 70-80% lỗ hổng phổ biến trước deploy
- Swagger/OpenAPI: document API tự động, team frontend không cần hỏi backend về endpoints
🧠 Kiểm Tra Kiến Thức Chương 7
Trả lời 5 câu để củng cố. Đạt ≥ 80% sẽ tự động đánh dấu hoàn thành chương.
1. Framework backend dùng trong khóa học là gì?
2. Prisma là gì?
3. JWT được dùng để làm gì?
4. Middleware nào thêm các HTTP header bảo mật cho Express?
5. OWASP Top 10 là gì?