Deep Dive: Phát Triển API Node.js với TypeScript

Chào Mừng Đến Deep Dive
Bạn đã học những kiến thức cơ bản về phát triển backend Node.js trong Phase 3. Bây giờ là lúc đi sâu hơn. Bài viết này covers các pattern production phân biệt giữa dự án hobby và API thực tế — middleware chain type-safe, runtime validation, centralized error handling, authentication bảo mật, file uploads, background jobs và structured logging.
Đây là các pattern được sử dụng tại các công ty như Stripe, Vercel và Linear. Thành thạo chúng, và bạn sẽ xây dựng API dễ bảo trì, bảo mật và dễ chịu khi làm việc.
Yêu cầu trước: Phase 3: Backend Development Thời gian: 2-3 giờ (làm theo các ví dụ)
Bạn Sẽ Học Được
✅ Express.js với TypeScript typing nghiêm ngặt (request, response, middleware)
✅ Fastify với type inference built-in và schema validation
✅ Request validation với Zod (body, params, query)
✅ Middleware patterns (auth, logging, rate limiting, error handling)
✅ Centralized error handling với custom error classes
✅ JWT authentication middleware với typed user context
✅ File uploads với Multer và type safety
✅ Background jobs với BullMQ
✅ Structured logging với Pino
✅ Xây dựng API template production-ready
1. Express.js Type-Safe Setup
Mở Rộng Express Types
Các types mặc định của Express khá lỏng lẻo. Hãy sửa điều đó bằng cách mở rộng object Request để bao gồm dữ liệu user đã xác thực:
// src/types/express.d.ts
import { JwtPayload } from "../auth/jwt";
declare global {
namespace Express {
interface Request {
user?: JwtPayload;
requestId: string;
}
}
}
export {};Typed Request Handlers
Tạo helper type bắt buộc shape của request body, params và query:
// src/types/handler.ts
import { Request, Response, NextFunction } from "express";
interface TypedRequest<
TBody = unknown,
TParams = Record<string, string>,
TQuery = Record<string, string>
> extends Request {
body: TBody;
params: TParams;
query: TQuery;
}
type AsyncHandler<
TBody = unknown,
TParams = Record<string, string>,
TQuery = Record<string, string>
> = (
req: TypedRequest<TBody, TParams, TQuery>,
res: Response,
next: NextFunction
) => Promise<void>;Sử Dụng Typed Handlers
// src/routes/users.ts
import { Router } from "express";
import { AsyncHandler } from "../types/handler";
interface CreateUserBody {
name: string;
email: string;
password: string;
}
interface UserParams {
id: string;
}
const createUser: AsyncHandler<CreateUserBody> = async (req, res) => {
const { name, email, password } = req.body; // Được typed đầy đủ
// ...logic tạo user
res.status(201).json({ id: "1", name, email });
};
const getUser: AsyncHandler<unknown, UserParams> = async (req, res) => {
const { id } = req.params; // string, đã typed
// ...logic lấy user
res.json({ id, name: "John" });
};
const router = Router();
router.post("/users", createUser);
router.get("/users/:id", getUser);
export default router;2. Fastify: Type Safety Built-in
Fastify có hỗ trợ TypeScript first-class. Không giống Express, types chảy xuyên suốt toàn bộ request lifecycle.
Fastify Setup Cơ Bản
// src/server.ts
import Fastify from "fastify";
const app = Fastify({
logger: true,
});
// Route với inline schema typing
app.get<{
Params: { id: string };
Querystring: { fields?: string };
Reply: { id: string; name: string; email: string };
}>("/users/:id", async (request, reply) => {
const { id } = request.params; // typed là string
const { fields } = request.query; // typed là string | undefined
return { id, name: "John", email: "[email protected]" };
});
app.listen({ port: 3000 });Fastify với JSON Schema Validation
Fastify validate requests và serialize responses dùng JSON Schema — tại runtime:
import Fastify, { FastifyInstance } from "fastify";
const app: FastifyInstance = Fastify({ logger: true });
// Định nghĩa schemas cho compile-time VÀ runtime safety
const createUserSchema = {
body: {
type: "object",
required: ["name", "email", "password"],
properties: {
name: { type: "string", minLength: 2 },
email: { type: "string", format: "email" },
password: { type: "string", minLength: 8 },
},
},
response: {
201: {
type: "object",
properties: {
id: { type: "string" },
name: { type: "string" },
email: { type: "string" },
},
},
},
} as const;
interface CreateUserBody {
name: string;
email: string;
password: string;
}
app.post<{ Body: CreateUserBody }>(
"/users",
{ schema: createUserSchema },
async (request, reply) => {
const { name, email, password } = request.body; // validated + typed
// ...tạo user
reply.status(201).send({ id: "1", name, email });
}
);Express vs Fastify: Khi Nào Chọn Cái Nào
| Tính năng | Express | Fastify |
|---|---|---|
| TypeScript | Cần @types/express + typing thủ công | Type inference built-in |
| Validation | Thư viện bên ngoài (Zod, Joi) | JSON Schema built-in |
| Performance | ~15,000 req/s | ~45,000 req/s |
| Middleware | .use() middleware chain | Plugin system với encapsulation |
| Ecosystem | Lớn nhất (npm) | Đang phát triển, tương thích Express middleware |
| Độ khó học | Dễ | Trung bình |
| Phù hợp cho | MVP nhanh, codebase Express có sẵn | API cần performance, dự án mới |
Khuyến nghị: Dùng Express nếu bạn đang làm việc với codebase Express có sẵn hoặc cần ecosystem lớn nhất. Dùng Fastify cho dự án mới nơi performance và type safety quan trọng.
3. Request Validation với Zod
Runtime validation rất quan trọng — TypeScript types biến mất sau khi compilation. Zod là cầu nối giữa compile-time types và runtime safety.
Thiết Lập Zod Schemas
// src/schemas/user.schema.ts
import { z } from "zod";
export const createUserSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email(),
password: z
.string()
.min(8)
.regex(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
"Phải chứa chữ hoa, chữ thường và số"
),
});
export const updateUserSchema = createUserSchema.partial().extend({
bio: z.string().max(500).optional(),
});
export const userParamsSchema = z.object({
id: z.string().uuid(),
});
export const listUsersQuerySchema = z.object({
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(["name", "email", "createdAt"]).default("createdAt"),
order: z.enum(["asc", "desc"]).default("desc"),
search: z.string().optional(),
});
// Suy luận TypeScript types từ schemas
export type CreateUserInput = z.infer<typeof createUserSchema>;
export type UpdateUserInput = z.infer<typeof updateUserSchema>;
export type UserParams = z.infer<typeof userParamsSchema>;
export type ListUsersQuery = z.infer<typeof listUsersQuerySchema>;Validation Middleware
Tạo middleware tái sử dụng để validate bất kỳ phần nào của request:
// src/middleware/validate.ts
import { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
interface ValidationSchemas {
body?: AnyZodObject;
params?: AnyZodObject;
query?: AnyZodObject;
}
export function validate(schemas: ValidationSchemas) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
if (schemas.body) {
req.body = await schemas.body.parseAsync(req.body);
}
if (schemas.params) {
req.params = await schemas.params.parseAsync(req.params);
}
if (schemas.query) {
req.query = await schemas.query.parseAsync(req.query);
}
next();
} catch (error) {
if (error instanceof ZodError) {
res.status(400).json({
error: "Validation thất bại",
details: error.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
})),
});
return;
}
next(error);
}
};
}Sử Dụng Validation Trong Routes
// src/routes/users.ts
import { Router } from "express";
import { validate } from "../middleware/validate";
import {
createUserSchema,
updateUserSchema,
userParamsSchema,
listUsersQuerySchema,
} from "../schemas/user.schema";
import type { CreateUserInput, UserParams, ListUsersQuery } from "../schemas/user.schema";
const router = Router();
// POST /users - validated body
router.post(
"/users",
validate({ body: createUserSchema }),
async (req, res) => {
const data = req.body as CreateUserInput; // an toàn sau validation
// ...tạo user
res.status(201).json({ id: "1", ...data });
}
);
// GET /users - validated query params
router.get(
"/users",
validate({ query: listUsersQuerySchema }),
async (req, res) => {
const { page, limit, sort, order, search } = req.query as unknown as ListUsersQuery;
// Tất cả đã typed và validated: page là number, sort là "name" | "email" | "createdAt"
res.json({ page, limit, data: [] });
}
);
// PUT /users/:id - validated params + body
router.put(
"/users/:id",
validate({ params: userParamsSchema, body: updateUserSchema }),
async (req, res) => {
const { id } = req.params as unknown as UserParams;
// id được đảm bảo là UUID hợp lệ
res.json({ id, ...req.body });
}
);
export default router;4. Middleware Patterns
Định Nghĩa Middleware Types
// src/types/middleware.ts
import { Request, Response, NextFunction } from "express";
// Middleware tiêu chuẩn
type Middleware = (req: Request, res: Response, next: NextFunction) => void;
// Async middleware (bắt promise rejections)
type AsyncMiddleware = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
// Error middleware (4 tham số)
type ErrorMiddleware = (
err: Error,
req: Request,
res: Response,
next: NextFunction
) => void;Async Wrapper
Express không bắt rejected promises từ async handlers. Sửa điều này với wrapper:
// src/middleware/asyncHandler.ts
import { Request, Response, NextFunction } from "express";
type AsyncRequestHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise<void>;
export function asyncHandler(fn: AsyncRequestHandler) {
return (req: Request, res: Response, next: NextFunction) => {
fn(req, res, next).catch(next);
};
}
// Sử dụng
router.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError("User not found");
}
res.json(user);
})
);Request ID Middleware
Theo dõi mọi request xuyên suốt hệ thống:
// src/middleware/requestId.ts
import { randomUUID } from "crypto";
import { Request, Response, NextFunction } from "express";
export function requestId(req: Request, res: Response, next: NextFunction) {
const id = (req.headers["x-request-id"] as string) || randomUUID();
req.requestId = id;
res.setHeader("x-request-id", id);
next();
}Rate Limiting Middleware
// src/middleware/rateLimit.ts
import { Request, Response, NextFunction } from "express";
interface RateLimitConfig {
windowMs: number;
maxRequests: number;
}
interface RateLimitEntry {
count: number;
resetTime: number;
}
export function rateLimit({ windowMs, maxRequests }: RateLimitConfig) {
const store = new Map<string, RateLimitEntry>();
// Dọn dẹp các entry hết hạn định kỳ
setInterval(() => {
const now = Date.now();
for (const [key, entry] of store) {
if (entry.resetTime < now) {
store.delete(key);
}
}
}, windowMs);
return (req: Request, res: Response, next: NextFunction) => {
const key = req.ip || "unknown";
const now = Date.now();
const entry = store.get(key);
if (!entry || entry.resetTime < now) {
store.set(key, { count: 1, resetTime: now + windowMs });
next();
return;
}
if (entry.count >= maxRequests) {
res.status(429).json({
error: "Quá nhiều requests",
retryAfter: Math.ceil((entry.resetTime - now) / 1000),
});
return;
}
entry.count++;
next();
};
}
// Sử dụng
app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, maxRequests: 10 }));
app.use("/api", rateLimit({ windowMs: 60 * 1000, maxRequests: 100 }));Kết Hợp Middleware
Chain middleware theo cách dễ đọc, type-safe:
// src/middleware/compose.ts
import { RequestHandler } from "express";
export function compose(...middlewares: RequestHandler[]): RequestHandler[] {
return middlewares;
}
// Sử dụng: định nghĩa route gọn gàng
router.post(
"/posts",
...compose(
authenticate,
authorize("admin", "editor"),
validate({ body: createPostSchema }),
),
asyncHandler(postController.create)
);5. Centralized Error Handling
Custom Error Classes
// src/errors/AppError.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number,
public code: string,
public details?: Record<string, unknown>
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, "NOT_FOUND");
}
}
export class ValidationError extends AppError {
constructor(details: Record<string, unknown>) {
super("Validation thất bại", 400, "VALIDATION_ERROR", details);
}
}
export class UnauthorizedError extends AppError {
constructor(message = "Yêu cầu xác thực") {
super(message, 401, "UNAUTHORIZED");
}
}
export class ForbiddenError extends AppError {
constructor(message = "Không đủ quyền") {
super(message, 403, "FORBIDDEN");
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(message, 409, "CONFLICT");
}
}Global Error Handler
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from "express";
import { AppError } from "../errors/AppError";
import { ZodError } from "zod";
import { logger } from "../lib/logger";
export function errorHandler(
err: Error,
req: Request,
res: Response,
_next: NextFunction
) {
// Log lỗi
logger.error({
err,
requestId: req.requestId,
method: req.method,
url: req.url,
});
// Lỗi ứng dụng đã biết
if (err instanceof AppError) {
res.status(err.statusCode).json({
error: {
code: err.code,
message: err.message,
...(err.details && { details: err.details }),
},
});
return;
}
// Lỗi Zod validation
if (err instanceof ZodError) {
res.status(400).json({
error: {
code: "VALIDATION_ERROR",
message: "Dữ liệu request không hợp lệ",
details: err.errors.map((e) => ({
field: e.path.join("."),
message: e.message,
})),
},
});
return;
}
// Lỗi không xác định — không để lộ chi tiết nội bộ
res.status(500).json({
error: {
code: "INTERNAL_ERROR",
message: "Đã xảy ra lỗi không mong muốn",
},
});
}Sử Dụng Errors Trong Services
// src/services/user.service.ts
import { NotFoundError, ConflictError } from "../errors/AppError";
import { prisma } from "../lib/prisma";
import type { CreateUserInput } from "../schemas/user.schema";
export class UserService {
async create(data: CreateUserInput) {
const existing = await prisma.user.findUnique({
where: { email: data.email },
});
if (existing) {
throw new ConflictError("Email đã được đăng ký");
}
return prisma.user.create({ data });
}
async findById(id: string) {
const user = await prisma.user.findUnique({ where: { id } });
if (!user) {
throw new NotFoundError("User");
}
return user;
}
}6. JWT Authentication Middleware
JWT Utility Functions
// src/auth/jwt.ts
import jwt from "jsonwebtoken";
const JWT_SECRET = process.env.JWT_SECRET!;
const JWT_EXPIRES_IN = "24h";
const REFRESH_TOKEN_EXPIRES_IN = "7d";
export interface JwtPayload {
userId: string;
email: string;
role: "admin" | "user" | "editor";
}
export interface TokenPair {
accessToken: string;
refreshToken: string;
}
export function generateTokens(payload: JwtPayload): TokenPair {
const accessToken = jwt.sign(payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
});
const refreshToken = jwt.sign(
{ userId: payload.userId, type: "refresh" },
JWT_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES_IN }
);
return { accessToken, refreshToken };
}
export function verifyToken(token: string): JwtPayload {
return jwt.verify(token, JWT_SECRET) as JwtPayload;
}Authentication Middleware
// src/middleware/authenticate.ts
import { Request, Response, NextFunction } from "express";
import { verifyToken } from "../auth/jwt";
import { UnauthorizedError } from "../errors/AppError";
export function authenticate(req: Request, _res: Response, next: NextFunction) {
const header = req.headers.authorization;
if (!header?.startsWith("Bearer ")) {
throw new UnauthorizedError("Thiếu hoặc sai authorization header");
}
const token = header.slice(7);
try {
const payload = verifyToken(token);
req.user = payload; // Có sẵn trong tất cả handlers tiếp theo
next();
} catch {
throw new UnauthorizedError("Token không hợp lệ hoặc đã hết hạn");
}
}Authorization Middleware
// src/middleware/authorize.ts
import { Request, Response, NextFunction } from "express";
import { ForbiddenError, UnauthorizedError } from "../errors/AppError";
type Role = "admin" | "user" | "editor";
export function authorize(...allowedRoles: Role[]) {
return (req: Request, _res: Response, next: NextFunction) => {
if (!req.user) {
throw new UnauthorizedError();
}
if (!allowedRoles.includes(req.user.role)) {
throw new ForbiddenError(
`Role '${req.user.role}' không thể truy cập resource này`
);
}
next();
};
}
// Sử dụng
router.delete(
"/users/:id",
authenticate,
authorize("admin"),
asyncHandler(async (req, res) => {
await userService.delete(req.params.id);
res.status(204).send();
})
);Protected Route Pattern
// src/routes/protected.ts
import { Router } from "express";
import { authenticate } from "../middleware/authenticate";
import { authorize } from "../middleware/authorize";
import { asyncHandler } from "../middleware/asyncHandler";
const router = Router();
// Tất cả routes trong router này yêu cầu authentication
router.use(authenticate);
// GET /me — bất kỳ user đã xác thực nào
router.get(
"/me",
asyncHandler(async (req, res) => {
const user = await userService.findById(req.user!.userId);
res.json(user);
})
);
// Routes chỉ dành cho admin
router.get(
"/admin/stats",
authorize("admin"),
asyncHandler(async (req, res) => {
const stats = await adminService.getStats();
res.json(stats);
})
);
export default router;7. File Uploads với Multer
Cấu Hình Multer
// src/middleware/upload.ts
import multer, { FileFilterCallback } from "multer";
import { Request } from "express";
import path from "path";
import { randomUUID } from "crypto";
// Định nghĩa loại file được phép
const ALLOWED_MIME_TYPES = [
"image/jpeg",
"image/png",
"image/webp",
"application/pdf",
] as const;
type AllowedMimeType = (typeof ALLOWED_MIME_TYPES)[number];
function isAllowedMimeType(mime: string): mime is AllowedMimeType {
return ALLOWED_MIME_TYPES.includes(mime as AllowedMimeType);
}
// File filter
const fileFilter = (
_req: Request,
file: Express.Multer.File,
cb: FileFilterCallback
) => {
if (isAllowedMimeType(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`Loại file ${file.mimetype} không được phép`));
}
};
// Cấu hình storage
const storage = multer.diskStorage({
destination: (_req, _file, cb) => {
cb(null, "uploads/");
},
filename: (_req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `${randomUUID()}${ext}`);
},
});
// Export uploaders đã cấu hình
export const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5 MB
files: 5, // Tối đa 5 files mỗi request
},
});Upload Routes
// src/routes/upload.ts
import { Router } from "express";
import { upload } from "../middleware/upload";
import { authenticate } from "../middleware/authenticate";
import { asyncHandler } from "../middleware/asyncHandler";
const router = Router();
// Upload một file
router.post(
"/avatar",
authenticate,
upload.single("avatar"),
asyncHandler(async (req, res) => {
if (!req.file) {
res.status(400).json({ error: "Không có file được upload" });
return;
}
const { filename, mimetype, size } = req.file;
// Lưu tham chiếu file vào database
const url = `/uploads/${filename}`;
res.json({ url, mimetype, size });
})
);
// Upload nhiều files
router.post(
"/gallery",
authenticate,
upload.array("images", 5),
asyncHandler(async (req, res) => {
const files = req.files as Express.Multer.File[];
const uploaded = files.map((file) => ({
url: `/uploads/${file.filename}`,
mimetype: file.mimetype,
size: file.size,
}));
res.json({ files: uploaded });
})
);
export default router;8. Background Jobs với BullMQ
Thiết Lập Queue
// src/jobs/queue.ts
import { Queue, Worker, Job } from "bullmq";
import IORedis from "ioredis";
const connection = new IORedis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
});
// Định nghĩa job types với union
interface EmailJobData {
type: "email";
to: string;
subject: string;
body: string;
}
interface ImageResizeJobData {
type: "image-resize";
path: string;
width: number;
height: number;
}
interface ReportJobData {
type: "report";
userId: string;
startDate: string;
endDate: string;
}
type JobData = EmailJobData | ImageResizeJobData | ReportJobData;
// Tạo typed queue
export const taskQueue = new Queue<JobData>("tasks", { connection });
// Type-safe job dispatchers
export async function sendEmail(data: Omit<EmailJobData, "type">) {
return taskQueue.add("email", { type: "email", ...data }, {
attempts: 3,
backoff: { type: "exponential", delay: 1000 },
});
}
export async function resizeImage(data: Omit<ImageResizeJobData, "type">) {
return taskQueue.add("image-resize", { type: "image-resize", ...data });
}
export async function generateReport(data: Omit<ReportJobData, "type">) {
return taskQueue.add("report", { type: "report", ...data }, {
attempts: 2,
backoff: { type: "fixed", delay: 5000 },
});
}Worker với Type-Safe Processing
// src/jobs/worker.ts
import { Worker, Job } from "bullmq";
import IORedis from "ioredis";
import { logger } from "../lib/logger";
const connection = new IORedis(process.env.REDIS_URL!, {
maxRetriesPerRequest: null,
});
async function processEmail(data: { to: string; subject: string; body: string }) {
// Logic gửi email (vd: với Resend, SendGrid)
logger.info({ to: data.to, subject: data.subject }, "Email đã gửi");
}
async function processImageResize(data: { path: string; width: number; height: number }) {
// Logic resize ảnh (vd: với Sharp)
logger.info({ path: data.path, width: data.width }, "Ảnh đã resize");
}
async function processReport(data: { userId: string; startDate: string; endDate: string }) {
// Logic tạo báo cáo
logger.info({ userId: data.userId }, "Báo cáo đã tạo");
}
const worker = new Worker(
"tasks",
async (job: Job) => {
const { type, ...data } = job.data;
switch (type) {
case "email":
await processEmail(data as { to: string; subject: string; body: string });
break;
case "image-resize":
await processImageResize(data as { path: string; width: number; height: number });
break;
case "report":
await processReport(data as { userId: string; startDate: string; endDate: string });
break;
default:
throw new Error(`Loại job không xác định: ${type}`);
}
},
{ connection, concurrency: 5 }
);
worker.on("completed", (job) => {
logger.info({ jobId: job.id, type: job.data.type }, "Job hoàn thành");
});
worker.on("failed", (job, err) => {
logger.error({ jobId: job?.id, error: err.message }, "Job thất bại");
});
export default worker;Sử Dụng Jobs Trong Routes
// src/routes/admin.ts
import { Router } from "express";
import { sendEmail, generateReport } from "../jobs/queue";
import { authenticate } from "../middleware/authenticate";
import { authorize } from "../middleware/authorize";
import { asyncHandler } from "../middleware/asyncHandler";
const router = Router();
router.post(
"/invite",
authenticate,
authorize("admin"),
asyncHandler(async (req, res) => {
const { email, name } = req.body;
// Queue email — không block response
await sendEmail({
to: email,
subject: "Bạn được mời!",
body: `Xin chào ${name}, bạn đã được mời tham gia nền tảng của chúng tôi.`,
});
res.json({ message: "Lời mời đã gửi" });
})
);
router.post(
"/reports",
authenticate,
authorize("admin"),
asyncHandler(async (req, res) => {
const { startDate, endDate } = req.body;
const job = await generateReport({
userId: req.user!.userId,
startDate,
endDate,
});
res.json({ message: "Báo cáo đang xử lý", jobId: job.id });
})
);
export default router;9. Structured Logging với Pino
Cấu Hình Logger
// src/lib/logger.ts
import pino from "pino";
export const logger = pino({
level: process.env.LOG_LEVEL || "info",
transport:
process.env.NODE_ENV === "development"
? {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "HH:MM:ss",
ignore: "pid,hostname",
},
}
: undefined,
// Production: JSON output cho log aggregation (ELK, Datadog)
serializers: {
req: pino.stdSerializers.req,
res: pino.stdSerializers.res,
err: pino.stdSerializers.err,
},
});
// Tạo child loggers cho các module khác nhau
export const dbLogger = logger.child({ module: "database" });
export const authLogger = logger.child({ module: "auth" });
export const jobLogger = logger.child({ module: "jobs" });Request Logging Middleware
// src/middleware/requestLogger.ts
import { Request, Response, NextFunction } from "express";
import { logger } from "../lib/logger";
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
logger.info({
requestId: req.requestId,
method: req.method,
url: req.originalUrl,
statusCode: res.statusCode,
duration: `${duration}ms`,
userAgent: req.headers["user-agent"],
ip: req.ip,
...(req.user && { userId: req.user.userId }),
});
});
next();
}Best Practices Structured Logging
// KHÔNG NÊN: Nối chuỗi (khó parse, search, filter)
logger.info("User " + userId + " created post " + postId);
// NÊN: Dữ liệu có cấu trúc (searchable, filterable trong log aggregation)
logger.info({ userId, postId, action: "create_post" }, "Post created");
// KHÔNG NÊN: Log dữ liệu nhạy cảm
logger.info({ email, password }, "Login attempt");
// NÊN: Ẩn các trường nhạy cảm
logger.info({ email, hasPassword: !!password }, "Login attempt");
// NÊN: Dùng child loggers cho context
const reqLogger = logger.child({ requestId: req.requestId, userId: req.user?.userId });
reqLogger.info("Processing order");
reqLogger.info({ orderId: "123" }, "Order processed");
// Tất cả logs tự động bao gồm requestId và userId10. Ghép Tất Cả Lại: API Template
Application Setup
// src/app.ts
import express from "express";
import cors from "cors";
import helmet from "helmet";
import { requestId } from "./middleware/requestId";
import { requestLogger } from "./middleware/requestLogger";
import { errorHandler } from "./middleware/errorHandler";
import { rateLimit } from "./middleware/rateLimit";
import userRoutes from "./routes/users";
import authRoutes from "./routes/auth";
import uploadRoutes from "./routes/upload";
import adminRoutes from "./routes/admin";
const app = express();
// --- Global Middleware ---
app.use(helmet()); // Security headers
app.use(cors({ origin: process.env.CORS_ORIGIN || "*" }));
app.use(express.json({ limit: "10mb" }));
app.use(requestId);
app.use(requestLogger);
// --- Rate Limiting ---
app.use("/api/auth", rateLimit({ windowMs: 15 * 60 * 1000, maxRequests: 20 }));
app.use("/api", rateLimit({ windowMs: 60 * 1000, maxRequests: 100 }));
// --- Routes ---
app.use("/api/auth", authRoutes);
app.use("/api/users", userRoutes);
app.use("/api/upload", uploadRoutes);
app.use("/api/admin", adminRoutes);
// --- Health Check ---
app.get("/health", (_req, res) => {
res.json({
status: "healthy",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
});
// --- Error Handler (phải đặt cuối cùng) ---
app.use(errorHandler);
export default app;Server Entry Point
// src/index.ts
import app from "./app";
import { logger } from "./lib/logger";
const PORT = parseInt(process.env.PORT || "3000", 10);
async function main() {
// Khởi động server
app.listen(PORT, () => {
logger.info({ port: PORT, env: process.env.NODE_ENV }, "Server đã khởi động");
});
// Graceful shutdown
const shutdown = async (signal: string) => {
logger.info({ signal }, "Đang tắt server một cách graceful");
// Đóng server, database connections, job queues, etc.
process.exit(0);
};
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));
}
main().catch((err) => {
logger.fatal({ err }, "Không thể khởi động server");
process.exit(1);
});Cấu Trúc Project
src/
├── index.ts # Entry point
├── app.ts # Express app setup
├── auth/
│ └── jwt.ts # JWT utilities
├── errors/
│ └── AppError.ts # Custom error classes
├── jobs/
│ ├── queue.ts # Job queue + dispatchers
│ └── worker.ts # Job processor
├── lib/
│ ├── logger.ts # Pino logger
│ └── prisma.ts # Prisma client
├── middleware/
│ ├── asyncHandler.ts # Async error wrapper
│ ├── authenticate.ts # JWT auth
│ ├── authorize.ts # Role-based access
│ ├── errorHandler.ts # Global error handler
│ ├── rateLimit.ts # Rate limiting
│ ├── requestId.ts # Request tracing
│ ├── requestLogger.ts # HTTP logging
│ ├── upload.ts # File upload (Multer)
│ └── validate.ts # Zod validation
├── routes/
│ ├── admin.ts # Admin routes
│ ├── auth.ts # Auth routes
│ ├── upload.ts # Upload routes
│ └── users.ts # User routes
├── schemas/
│ └── user.schema.ts # Zod schemas + types
├── services/
│ └── user.service.ts # Business logic
└── types/
├── express.d.ts # Express augmentation
├── handler.ts # Typed request helpers
└── middleware.ts # Middleware typesNhững Lỗi Thường Gặp
1. Không Validate Tại Runtime
// XẤU: TypeScript types không tồn tại tại runtime
interface CreateUserBody {
email: string;
}
router.post("/users", (req, res) => {
const { email } = req.body as CreateUserBody;
// email có thể là bất kỳ thứ gì — number, null, undefined
});
// TỐT: Validate với Zod, type được đảm bảo
const schema = z.object({ email: z.string().email() });
router.post("/users", validate({ body: schema }), (req, res) => {
const { email } = req.body; // Đã thực sự được validate
});2. Quên Bắt Async Errors
// XẤU: Unhandled promise rejection crash server
router.get("/users/:id", async (req, res) => {
const user = await db.user.findUnique({ where: { id: req.params.id } });
res.json(user); // Nếu db throw lỗi thì sao?
});
// TỐT: Dùng asyncHandler wrapper
router.get(
"/users/:id",
asyncHandler(async (req, res) => {
const user = await db.user.findUnique({ where: { id: req.params.id } });
res.json(user); // Errors được chuyển đến error handler
})
);3. Để Lộ Chi Tiết Lỗi Trong Production
// XẤU: Lộ stack traces và lỗi nội bộ
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
res.status(500).json({ error: err.message, stack: err.stack });
});
// TỐT: Dùng AppError cho lỗi đã biết, message chung cho lỗi không xác định
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
if (err instanceof AppError) {
res.status(err.statusCode).json({ error: { code: err.code, message: err.message } });
} else {
res.status(500).json({ error: { code: "INTERNAL_ERROR", message: "Đã xảy ra lỗi không mong muốn" } });
}
});4. Dùng any Để Tắt Type Errors
// XẤU: Đánh bại mục đích của TypeScript
const handler = (req: any, res: any) => {
res.json(req.body.whatever);
};
// TỐT: Typing đúng cách hoặc dùng unknown + validation
const handler = (req: Request, res: Response) => {
const result = createUserSchema.safeParse(req.body);
if (!result.success) {
res.status(400).json({ errors: result.error.flatten() });
return;
}
res.json(result.data); // Đã typed!
};Tóm Tắt và Điểm Chính
✅ Express typing: Mở rộng interface Request, tạo helpers TypedRequest cho body/params/query
✅ Fastify: Type inference built-in — ít boilerplate, DX tốt hơn cho dự án mới
✅ Zod validation: Cầu nối compile-time types và runtime safety với z.infer<typeof schema>
✅ Middleware: Compose authentication, authorization, validation và rate limiting gọn gàng
✅ Error handling: Dùng custom error classes + centralized error handler — không bao giờ để lộ nội bộ
✅ Authentication: JWT với typed payload, chuỗi middleware authenticate + authorize
✅ File uploads: Multer với typed configuration và file filtering
✅ Background jobs: BullMQ với discriminated union job types và type-safe dispatchers
✅ Logging: Pino structured logging — child loggers, redaction, JSON cho production
Tiếp Theo
- Deep Dive: Database & ORMs → — Prisma và Drizzle ORM patterns với TypeScript
- Deep Dive: Testing & DevOps → — Vitest, Playwright, CI/CD, Docker
Tài Liệu Tham Khảo
- Express TypeScript Guide
- Fastify TypeScript Documentation
- Zod Documentation
- BullMQ Documentation
- Pino Logger
Series: TypeScript Full-Stack Roadmap Trước đó: Deep Dive: React với TypeScript Tiếp theo: Deep Dive: Database & ORMs
📬 Đăng ký nhận bản tin
Nhận những bài viết mới nhất qua email mỗi tuần. Không spam, hủy đăng ký bất cứ lúc nào.
Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất cứ lúc nào.
💬 Comments
Đăng nhập để bình luận
Chúng tôi sẽ không đăng bất cứ điều gì mà không có sự cho phép của bạn.