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
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": {...}}
- 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