📙 Phần 3 · Xây Dựng Web · Chương 7

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.

5 giờ học
📝 3 bài học
🎯 REST API hoàn chỉnh
📊 Mức độ: Trung cấp → Nâng cao

🎯 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
1

Bài 7.1 — Express.js REST API

REST API
Kiến trúc API dùng HTTP methods (GET/POST/PUT/DELETE) để thao tác tài nguyên. URL đại diện cho resource, method đại diện cho action.
Middleware
Hàm chạy giữa request và response. Dùng để: xác thực token, validate input, log requests, xử lý lỗi tập trung.
Controller
Hàm xử lý business logic cho một route cụ thể. Nhận req, xử lý data, trả về res. Tách riêng khỏi route definition.
JWT (JSON Web Token)
Chuỗi token mã hóa chứa user info. Server ký với secret key, client gửi kèm mỗi request trong header Authorization.
ORM (Object-Relational Mapper)
Thư viện ánh xạ database tables thành objects. Prisma: type-safe, auto-complete, chống SQL injection mặc định.
Rate Limiting
Giới hạn số request/thời gian từ 1 IP. Chống brute force, DDoS. Ví dụ: 100 requests/15 phút.

Quy Trình Thiết Kế API Trước Khi Code

1
Xác định Resources

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.

2
Define Endpoints

Mỗi resource có 5 operations chuẩn: list, create, get-one, update, delete. URL dạng /api/posts/api/posts/:id.

3
Thiết kế Database Schema

Từ resources, define Prisma schema: models, fields, relationships. Hỏi Copilot: "Thiết kế Prisma schema cho [list resources]".

4
Viết API Contract

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.

5
Code từng endpoint

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.

6
Add Security Layer

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

text
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

javascript — src/app.js
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;
javascript — src/middleware/errorHandler.js
/**
 * 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;

2

Bài 7.2 — Database với Prisma ORM

HTTP MethodHành động CRUDEndpoint mẫuResponse code
GETRead (lấy dữ liệu)GET /api/posts200 OK
POSTCreate (tạo mới)POST /api/posts201 Created
PUTReplace (thay toàn bộ)PUT /api/posts/:id200 OK
PATCHUpdate (sửa một phần)PATCH /api/posts/:id200 OK
DELETEDelete (xóa)DELETE /api/posts/:id204 No Content
HTTP Status Codes quan trọng
200 OKThành công201 CreatedTạo mới thành công
400 Bad RequestInput không hợp lệ401 UnauthorizedChưa xác thực
403 ForbiddenKhông có quyền404 Not FoundKhông tìm thấy
409 ConflictDữ liệu đã tồn tại429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorLỗi server503 Service UnavailableServer đ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.

bash
npm install prisma @prisma/client
npx prisma init --datasource-provider sqlite
prisma — prisma/schema.prisma
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")
}
bash — Migrate
# 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

javascript — controllers/postController.js
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 };

3

Bài 7.3 — Authentication với JWT

🔐
Security-Critical Code

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.

bash
npm install bcryptjs jsonwebtoken zod
javascript — controllers/authController.js
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 };
javascript — middleware/auth.js
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

http — api-test.http (dùng 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"]
}

4

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

bash — Setup project
# 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"
bash — Cấu trúc thư mục
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 — prisma/schema.prisma
// 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
}
bash — Tạo và migrate database
# 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

javascript — src/middleware/auth.js
// 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

javascript — src/controllers/authController.js
// 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

javascript — src/controllers/notesController.js
// 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 };
javascript — src/index.js (Entry Point)
// 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

bash — Khởi động server và test 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"
Backend API hoàn chỉnh!

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.

💡 Mẹo từ ThanhDoIT
  • 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.

5

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.

🔒
Security Checklist
  • Helmet: security HTTP headers
  • Rate limiting: chống brute force
  • Input validation + sanitization
  • SQL injection prevention (Prisma)
  • CORS đúng cấu hình
📊
API Best Practices
  • Versioning: /api/v1/
  • Consistent response format
  • Pagination chuẩn (cursor/offset)
  • HTTP status codes đúng
  • API documentation (Swagger)

Security Setup Hoàn Chỉnh

typescript — security.middleware.ts
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();
  }
];
⚠ OWASP Top 10 — Những Lỗ Hổng Phổ Biến Nhất (API phải tránh)
  • 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

typescript — response.helper.ts
// 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ể.
Security review bằng AI không thay thế pentest thực sự, nhưng bắt được 70-80% lỗ hổng phổ biến trước khi deploy.

API Documentation với Swagger/OpenAPI

bash — Setup Swagger tự động
npm install swagger-jsdoc swagger-ui-express
npm install -D @types/swagger-jsdoc @types/swagger-ui-express
typescript — swagger.config.ts
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ực Hành: Security Hardening Cho API
  1. Thêm Helmet + CORS + Rate Limiting vào Express app của bạn
  2. Hỏi Copilot: "Review auth endpoints của tôi theo OWASP Top 10"
  3. Thêm express-validator cho tất cả POST/PUT endpoints
  4. Implement consistent response format với helper functions
  5. Setup Swagger docs cho ít nhất 5 endpoints
  6. Kiểm tra: curl -s http://localhost:3000/api/docs → thấy Swagger UI
✅ API Của Bạn Đạt Chuẩn Khi:
  • 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 Tập Tổng Kết Chương 7 — REST API Production-Ready
  1. Setup Express + TypeScript + Prisma với cấu trúc MVC hoàn chỉnh
  2. Implement auth: register + login + JWT middleware + /me endpoint
  3. CRUD hoàn chỉnh cho 1 resource với authorization check (chỉ owner mới được sửa)
  4. Thêm security: Helmet + CORS + Rate Limiting + Validation
  5. Hỏi Copilot Chat: "Review toàn bộ auth flow của tôi, tìm security issues"
  6. 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
Zalo: 0898 619 966 Z Gọi: 0898 619 966