TypeScript: Xây Dựng REST API với Express.js

Trong bài trước, chúng ta đã setup một TypeScript project production-ready. Giờ là lúc build thứ gì đó thực sự — một REST API hoàn chỉnh với Express.js sử dụng mọi tool đã cấu hình.
Đây không phải ví dụ CRUD chơi chơi. Chúng ta sẽ build task management API với proper validation, error handling, layered architecture, và API documentation. Kiểu API mà bạn sẽ ship lên production.
Thời gian: 2-3 giờ
Điều kiện: TypeScript: Thiết Lập Project cho Node.js Backend
Những Gì Bạn Sẽ Học
✅ Express.js với TypeScript — type-safe routes, middleware, và error handling
✅ Request validation với Zod — schemas vừa validate runtime vừa infer types
✅ Typed request/response — không còn any trong handlers
✅ Middleware patterns — auth, logging, request ID, rate limiting
✅ Global error handling — tập trung, nhất quán error responses
✅ Controller/Service/Repository — separation of concerns sạch sẽ
✅ API versioning — future-proof endpoints
✅ OpenAPI/Swagger docs — auto-generated từ code
1. Project Setup
Bắt đầu từ setup trong Bài 1, rồi thêm Express và types:
npm install express zod
npm install -D @types/expressCài thêm utilities sẽ dùng xuyên suốt:
npm install uuid
npm install -D @types/uuidCấu trúc project khi hoàn thành sẽ như sau:
src/
├── index.ts # Entry point
├── app.ts # Express app setup
├── config/
│ └── env.ts # Environment validation (từ Bài 1)
├── routes/
│ ├── index.ts # Route aggregator
│ └── task.routes.ts # Task endpoints
├── controllers/
│ └── task.controller.ts # HTTP layer
├── services/
│ └── task.service.ts # Business logic
├── repositories/
│ └── task.repository.ts # Data access (in-memory tạm thời)
├── middleware/
│ ├── error-handler.ts # Global error handler
│ ├── validate.ts # Zod validation middleware
│ ├── request-id.ts # Request ID middleware
│ └── not-found.ts # 404 handler
├── schemas/
│ └── task.schema.ts # Zod schemas
├── types/
│ └── index.ts # Shared types
└── utils/
└── errors.ts # Custom error classes2. Custom Error Classes
Trước khi viết bất kỳ route nào, định nghĩa cách errors hoạt động. Đây là nền tảng cho consistent API responses.
// src/utils/errors.ts
export class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public code?: string,
) {
super(message);
this.name = "AppError";
}
}
export class NotFoundError extends AppError {
constructor(resource: string, id: string) {
super(404, `${resource} with id '${id}' not found`, "NOT_FOUND");
this.name = "NotFoundError";
}
}
export class ValidationError extends AppError {
constructor(
message: string,
public details?: Record<string, string[]>,
) {
super(400, message, "VALIDATION_ERROR");
this.name = "ValidationError";
}
}
export class ConflictError extends AppError {
constructor(message: string) {
super(409, message, "CONFLICT");
this.name = "ConflictError";
}
}Tại sao custom error classes? Vì throw new NotFoundError("Task", id) rõ ràng hơn vô cùng so với throw { status: 404, message: "..." }. Và error handler biết chính xác phải xử lý mỗi loại thế nào.
3. Zod Schemas — Validation + Types Cùng Lúc
Zod cho phép bạn định nghĩa schema một lần và nhận được cả runtime validation lẫn TypeScript types. Không duplicate.
// src/schemas/task.schema.ts
import { z } from "zod";
// === Base Schema ===
export const taskSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1, "Title is required").max(200),
description: z.string().max(2000).optional(),
status: z.enum(["todo", "in_progress", "done"]).default("todo"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
dueDate: z.coerce.date().optional(),
createdAt: z.date(),
updatedAt: z.date(),
});
// === Request Schemas ===
export const createTaskSchema = z.object({
body: z.object({
title: z.string().min(1, "Title is required").max(200),
description: z.string().max(2000).optional(),
status: z.enum(["todo", "in_progress", "done"]).default("todo"),
priority: z.enum(["low", "medium", "high"]).default("medium"),
dueDate: z.coerce.date().optional(),
}),
});
export const updateTaskSchema = z.object({
body: z.object({
title: z.string().min(1).max(200).optional(),
description: z.string().max(2000).optional(),
status: z.enum(["todo", "in_progress", "done"]).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
dueDate: z.coerce.date().optional(),
}),
params: z.object({
id: z.string().uuid("Invalid task ID format"),
}),
});
export const getTaskSchema = z.object({
params: z.object({
id: z.string().uuid("Invalid task ID format"),
}),
});
export const listTasksSchema = z.object({
query: z.object({
status: z.enum(["todo", "in_progress", "done"]).optional(),
priority: z.enum(["low", "medium", "high"]).optional(),
page: z.coerce.number().int().positive().default(1),
limit: z.coerce.number().int().positive().max(100).default(20),
sortBy: z.enum(["createdAt", "dueDate", "priority"]).default("createdAt"),
order: z.enum(["asc", "desc"]).default("desc"),
}),
});
// === Inferred Types ===
export type Task = z.infer<typeof taskSchema>;
export type CreateTaskInput = z.infer<typeof createTaskSchema>["body"];
export type UpdateTaskInput = z.infer<typeof updateTaskSchema>["body"];
export type ListTasksQuery = z.infer<typeof listTasksSchema>["query"];Điểm Mấu Chốt
Để ý createTaskSchema wrap body trong z.object({ body: ... }). Cấu trúc này khớp với Express request (req.body, req.params, req.query), nên validation middleware có thể validate cả ba cùng lúc.
Và types được infer từ schemas — không cần interface riêng phải giữ đồng bộ.
4. Validation Middleware
Middleware tái sử dụng validate bất kỳ Zod schema nào với request:
// src/middleware/validate.ts
import type { Request, Response, NextFunction } from "express";
import { AnyZodObject, ZodError } from "zod";
export function validate(schema: AnyZodObject) {
return (req: Request, _res: Response, next: NextFunction) => {
try {
schema.parse({
body: req.body,
query: req.query,
params: req.params,
});
next();
} catch (error) {
if (error instanceof ZodError) {
const details: Record<string, string[]> = {};
for (const issue of error.issues) {
const path = issue.path.join(".");
if (!details[path]) {
details[path] = [];
}
details[path].push(issue.message);
}
next({
statusCode: 400,
message: "Validation failed",
code: "VALIDATION_ERROR",
details,
});
return;
}
next(error);
}
};
}Cách dùng trong routes:
// Schema validate req.body, req.params, và req.query
router.post("/", validate(createTaskSchema), controller.create);
router.put("/:id", validate(updateTaskSchema), controller.update);
router.get("/", validate(listTasksSchema), controller.list);Nếu validation fail, response trông như thế này:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"body.title": ["Title is required"],
"body.priority": ["Invalid enum value. Expected 'low' | 'medium' | 'high', received 'urgent'"]
}
}
}5. Middleware Stack
Request ID
Mỗi request nhận một ID duy nhất. Thiết yếu cho debugging production — correlate logs, trace errors về từng request cụ thể.
// src/middleware/request-id.ts
import type { Request, Response, NextFunction } from "express";
import { v4 as uuidv4 } from "uuid";
export function requestId(req: Request, res: Response, next: NextFunction) {
const id = (req.headers["x-request-id"] as string) ?? uuidv4();
req.headers["x-request-id"] = id;
res.setHeader("x-request-id", id);
next();
}Request Logger
Log mỗi request với thông tin timing:
// src/middleware/logger.ts
import type { Request, Response, NextFunction } from "express";
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
res.on("finish", () => {
const duration = Date.now() - start;
const requestId = req.headers["x-request-id"] ?? "unknown";
console.log(
JSON.stringify({
method: req.method,
path: req.path,
status: res.statusCode,
duration: `${duration}ms`,
requestId,
}),
);
});
next();
}Global Error Handler
Lưới an toàn bắt mọi thứ:
// src/middleware/error-handler.ts
import type { Request, Response, NextFunction } from "express";
import { AppError } from "@/utils/errors.js";
interface ErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, string[]>;
requestId?: string;
};
}
export function errorHandler(
err: Error & { statusCode?: number; code?: string; details?: Record<string, string[]> },
req: Request,
res: Response<ErrorResponse>,
_next: NextFunction,
) {
const statusCode = err.statusCode ?? 500;
const code = err.code ?? "INTERNAL_ERROR";
const message = statusCode === 500 ? "Internal server error" : err.message;
const requestId = req.headers["x-request-id"] as string | undefined;
// Log full error cho 500s
if (statusCode === 500) {
console.error("Unhandled error:", {
message: err.message,
stack: err.stack,
requestId,
});
}
res.status(statusCode).json({
error: {
code,
message,
details: err.details,
requestId,
},
});
}404 Handler
Cho routes không tồn tại:
// src/middleware/not-found.ts
import type { Request, Response } from "express";
export function notFoundHandler(req: Request, res: Response) {
res.status(404).json({
error: {
code: "NOT_FOUND",
message: `Route ${req.method} ${req.path} not found`,
},
});
}6. Repository Layer — Data Access
Chúng ta sẽ dùng in-memory store tạm thời. Cái hay của repository pattern là khi swap sang PostgreSQL + Prisma sau, chỉ cần thay đổi đúng file này.
// src/repositories/task.repository.ts
import type { Task, CreateTaskInput, UpdateTaskInput, ListTasksQuery } from "@/schemas/task.schema.js";
import { v4 as uuidv4 } from "uuid";
// In-memory store (thay bằng database sau)
const tasks: Map<string, Task> = new Map();
export class TaskRepository {
async findMany(query: ListTasksQuery): Promise<{ tasks: Task[]; total: number }> {
let results = Array.from(tasks.values());
// Filter
if (query.status) {
results = results.filter((t) => t.status === query.status);
}
if (query.priority) {
results = results.filter((t) => t.priority === query.priority);
}
// Sort
results.sort((a, b) => {
const field = query.sortBy;
const aVal = a[field];
const bVal = b[field];
if (aVal === undefined || bVal === undefined) return 0;
if (aVal < bVal) return query.order === "asc" ? -1 : 1;
if (aVal > bVal) return query.order === "asc" ? 1 : -1;
return 0;
});
const total = results.length;
// Paginate
const offset = (query.page - 1) * query.limit;
results = results.slice(offset, offset + query.limit);
return { tasks: results, total };
}
async findById(id: string): Promise<Task | undefined> {
return tasks.get(id);
}
async create(data: CreateTaskInput): Promise<Task> {
const now = new Date();
const task: Task = {
id: uuidv4(),
...data,
status: data.status ?? "todo",
priority: data.priority ?? "medium",
createdAt: now,
updatedAt: now,
};
tasks.set(task.id, task);
return task;
}
async update(id: string, data: UpdateTaskInput): Promise<Task | undefined> {
const existing = tasks.get(id);
if (!existing) return undefined;
const updated: Task = {
...existing,
...data,
updatedAt: new Date(),
};
tasks.set(id, updated);
return updated;
}
async delete(id: string): Promise<boolean> {
return tasks.delete(id);
}
async existsByTitle(title: string, excludeId?: string): Promise<boolean> {
for (const task of tasks.values()) {
if (task.title === title && task.id !== excludeId) return true;
}
return false;
}
}7. Service Layer — Business Logic
Service layer xử lý business rules. Nó không biết gì về HTTP, Express, hay request/response objects.
// src/services/task.service.ts
import { TaskRepository } from "@/repositories/task.repository.js";
import { NotFoundError, ConflictError } from "@/utils/errors.js";
import type { Task, CreateTaskInput, UpdateTaskInput, ListTasksQuery } from "@/schemas/task.schema.js";
export class TaskService {
private repository = new TaskRepository();
async list(query: ListTasksQuery): Promise<{ tasks: Task[]; total: number; page: number; limit: number }> {
const { tasks, total } = await this.repository.findMany(query);
return {
tasks,
total,
page: query.page,
limit: query.limit,
};
}
async getById(id: string): Promise<Task> {
const task = await this.repository.findById(id);
if (!task) {
throw new NotFoundError("Task", id);
}
return task;
}
async create(data: CreateTaskInput): Promise<Task> {
// Business rule: không cho duplicate titles
const exists = await this.repository.existsByTitle(data.title);
if (exists) {
throw new ConflictError(`Task with title '${data.title}' already exists`);
}
return this.repository.create(data);
}
async update(id: string, data: UpdateTaskInput): Promise<Task> {
// Verify task tồn tại
await this.getById(id);
// Business rule: check title uniqueness nếu title đang được update
if (data.title) {
const exists = await this.repository.existsByTitle(data.title, id);
if (exists) {
throw new ConflictError(`Task with title '${data.title}' already exists`);
}
}
const updated = await this.repository.update(id, data);
if (!updated) {
throw new NotFoundError("Task", id);
}
return updated;
}
async delete(id: string): Promise<void> {
// Verify task tồn tại trước khi xóa
await this.getById(id);
await this.repository.delete(id);
}
}Để ý service:
- Throw domain-specific errors (
NotFoundError,ConflictError) - Enforce business rules (không cho duplicate titles)
- Return data sạch — không HTTP status codes, không response objects
8. Controller Layer — Xử Lý HTTP
Controllers phiên dịch giữa HTTP và business logic. Parse requests, gọi services, và format responses.
// src/controllers/task.controller.ts
import type { Request, Response, NextFunction } from "express";
import { TaskService } from "@/services/task.service.js";
import type { CreateTaskInput, UpdateTaskInput, ListTasksQuery } from "@/schemas/task.schema.js";
export class TaskController {
private service = new TaskService();
list = async (req: Request, res: Response, next: NextFunction) => {
try {
const query = req.query as unknown as ListTasksQuery;
const result = await this.service.list(query);
res.json({
data: result.tasks,
pagination: {
page: result.page,
limit: result.limit,
total: result.total,
totalPages: Math.ceil(result.total / result.limit),
},
});
} catch (error) {
next(error);
}
};
getById = async (req: Request, res: Response, next: NextFunction) => {
try {
const task = await this.service.getById(req.params.id!);
res.json({ data: task });
} catch (error) {
next(error);
}
};
create = async (req: Request, res: Response, next: NextFunction) => {
try {
const input = req.body as CreateTaskInput;
const task = await this.service.create(input);
res.status(201).json({ data: task });
} catch (error) {
next(error);
}
};
update = async (req: Request, res: Response, next: NextFunction) => {
try {
const input = req.body as UpdateTaskInput;
const task = await this.service.update(req.params.id!, input);
res.json({ data: task });
} catch (error) {
next(error);
}
};
delete = async (req: Request, res: Response, next: NextFunction) => {
try {
await this.service.delete(req.params.id!);
res.status(204).send();
} catch (error) {
next(error);
}
};
}Pattern: Arrow Functions cho Methods
Để ý list = async (req, res, next) => { ... } thay vì async list(req, res, next) { ... }.
Arrow functions giữ this binding. Không có chúng, truyền controller.list cho Express như callback sẽ mất this context:
// BUG: `this` là undefined trong handler
router.get("/", controller.list); // controller.list là regular method
// WORKS: Arrow function capture `this` từ class
router.get("/", controller.list); // controller.list là arrow function property9. Routes
Nối mọi thứ lại với Express Router:
// src/routes/task.routes.ts
import { Router } from "express";
import { TaskController } from "@/controllers/task.controller.js";
import { validate } from "@/middleware/validate.js";
import {
createTaskSchema,
updateTaskSchema,
getTaskSchema,
listTasksSchema,
} from "@/schemas/task.schema.js";
const router = Router();
const controller = new TaskController();
router.get("/", validate(listTasksSchema), controller.list);
router.get("/:id", validate(getTaskSchema), controller.getById);
router.post("/", validate(createTaskSchema), controller.create);
router.put("/:id", validate(updateTaskSchema), controller.update);
router.delete("/:id", validate(getTaskSchema), controller.delete);
export default router;Route Aggregator với API Versioning
// src/routes/index.ts
import { Router } from "express";
import taskRoutes from "@/routes/task.routes.js";
const router = Router();
// API v1
router.use("/tasks", taskRoutes);
// Health check (không version)
router.get("/health", (_req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
});
});
export default router;Chiến Lược API Versioning
Mount route aggregator dưới versioned prefix:
// Trong app.ts (sẽ hiển thị đầy đủ bên dưới)
app.use("/api/v1", routes);Cho bạn endpoints như:
GET /api/v1/tasksPOST /api/v1/tasksGET /api/v1/health
Khi cần v2, tạo routes mới và mount song song v1:
app.use("/api/v1", routesV1);
app.use("/api/v2", routesV2); // Version mới, cái cũ vẫn hoạt động10. App Setup
Ghép tất cả lại:
// src/app.ts
import express from "express";
import routes from "@/routes/index.js";
import { requestId } from "@/middleware/request-id.js";
import { requestLogger } from "@/middleware/logger.js";
import { errorHandler } from "@/middleware/error-handler.js";
import { notFoundHandler } from "@/middleware/not-found.js";
const app = express();
// === Core Middleware ===
app.use(express.json({ limit: "10mb" }));
app.use(express.urlencoded({ extended: true }));
// === Custom Middleware ===
app.use(requestId);
app.use(requestLogger);
// === Routes ===
app.use("/api/v1", routes);
// === Error Handling (phải cuối cùng) ===
app.use(notFoundHandler);
app.use(errorHandler);
export default app;// src/index.ts
import app from "@/app.js";
import { env } from "@/config/env.js";
app.listen(env.PORT, env.HOST, () => {
console.log(`Server running at http://${env.HOST}:${env.PORT}`);
console.log(`Environment: ${env.NODE_ENV}`);
console.log(`API docs: http://${env.HOST}:${env.PORT}/api/v1/health`);
});Thứ Tự Middleware Rất Quan Trọng
Thứ tự middleware được đăng ký là thứ tự nó chạy:
- Body parsing đầu tiên — middleware phía sau cần
req.body - Request ID sớm — tất cả middleware và logs có thể reference nó
- Logger trước routes — capture timing cho toàn bộ request
- Routes — business logic của bạn
- 404 handler sau routes — bắt paths không khớp
- Error handler cuối cùng — bắt thrown errors từ bất kỳ đâu phía trên
11. Test API
Khởi động server:
npm run devTạo Task
curl -X POST http://localhost:3000/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{
"title": "Learn TypeScript",
"description": "Complete the TypeScript backend series",
"priority": "high",
"dueDate": "2026-03-15"
}'Response:
{
"data": {
"id": "a1b2c3d4-...",
"title": "Learn TypeScript",
"description": "Complete the TypeScript backend series",
"status": "todo",
"priority": "high",
"dueDate": "2026-03-15T00:00:00.000Z",
"createdAt": "2026-03-01T10:00:00.000Z",
"updatedAt": "2026-03-01T10:00:00.000Z"
}
}Danh Sách Tasks với Filters
# Tất cả tasks, trang 1
curl http://localhost:3000/api/v1/tasks
# Filter theo status
curl "http://localhost:3000/api/v1/tasks?status=todo&priority=high"
# Phân trang
curl "http://localhost:3000/api/v1/tasks?page=1&limit=10&sortBy=dueDate&order=asc"Update Task
curl -X PUT http://localhost:3000/api/v1/tasks/a1b2c3d4-... \
-H "Content-Type: application/json" \
-d '{"status": "in_progress"}'Xóa Task
curl -X DELETE http://localhost:3000/api/v1/tasks/a1b2c3d4-...
# 204 No ContentValidation Error
curl -X POST http://localhost:3000/api/v1/tasks \
-H "Content-Type: application/json" \
-d '{"title": "", "priority": "urgent"}'{
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"body.title": ["Title is required"],
"body.priority": ["Invalid enum value. Expected 'low' | 'medium' | 'high', received 'urgent'"]
}
}
}404 Error
curl http://localhost:3000/api/v1/tasks/non-existent-uuid{
"error": {
"code": "NOT_FOUND",
"message": "Task with id 'non-existent-uuid' not found",
"requestId": "f47ac10b-..."
}
}12. OpenAPI/Swagger Documentation
Auto-generate API docs để consumers không phải đoán endpoints.
npm install swagger-ui-express swagger-jsdoc
npm install -D @types/swagger-ui-express @types/swagger-jsdocTạo OpenAPI spec:
// src/config/swagger.ts
import swaggerJsdoc from "swagger-jsdoc";
const options: swaggerJsdoc.Options = {
definition: {
openapi: "3.0.0",
info: {
title: "Task Management API",
version: "1.0.0",
description: "REST API quản lý tasks, build với Express.js và TypeScript",
},
servers: [
{
url: "/api/v1",
description: "API v1",
},
],
components: {
schemas: {
Task: {
type: "object",
properties: {
id: { type: "string", format: "uuid" },
title: { type: "string", maxLength: 200 },
description: { type: "string", maxLength: 2000 },
status: { type: "string", enum: ["todo", "in_progress", "done"] },
priority: { type: "string", enum: ["low", "medium", "high"] },
dueDate: { type: "string", format: "date-time" },
createdAt: { type: "string", format: "date-time" },
updatedAt: { type: "string", format: "date-time" },
},
},
CreateTask: {
type: "object",
required: ["title"],
properties: {
title: { type: "string", minLength: 1, maxLength: 200 },
description: { type: "string", maxLength: 2000 },
status: { type: "string", enum: ["todo", "in_progress", "done"], default: "todo" },
priority: { type: "string", enum: ["low", "medium", "high"], default: "medium" },
dueDate: { type: "string", format: "date-time" },
},
},
Error: {
type: "object",
properties: {
error: {
type: "object",
properties: {
code: { type: "string" },
message: { type: "string" },
details: { type: "object" },
requestId: { type: "string" },
},
},
},
},
},
},
},
apis: ["./src/routes/*.ts"],
};
export const swaggerSpec = swaggerJsdoc(options);Thêm JSDoc annotations vào routes:
// src/routes/task.routes.ts
import { Router } from "express";
import { TaskController } from "@/controllers/task.controller.js";
import { validate } from "@/middleware/validate.js";
import {
createTaskSchema,
updateTaskSchema,
getTaskSchema,
listTasksSchema,
} from "@/schemas/task.schema.js";
const router = Router();
const controller = new TaskController();
/**
* @openapi
* /tasks:
* get:
* summary: Danh sách tất cả tasks
* parameters:
* - in: query
* name: status
* schema:
* type: string
* enum: [todo, in_progress, done]
* - in: query
* name: priority
* schema:
* type: string
* enum: [low, medium, high]
* - in: query
* name: page
* schema:
* type: integer
* default: 1
* - in: query
* name: limit
* schema:
* type: integer
* default: 20
* responses:
* 200:
* description: Danh sách tasks với pagination
*/
router.get("/", validate(listTasksSchema), controller.list);
/**
* @openapi
* /tasks/{id}:
* get:
* summary: Lấy task theo ID
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 200:
* description: Chi tiết task
* 404:
* description: Task không tìm thấy
*/
router.get("/:id", validate(getTaskSchema), controller.getById);
/**
* @openapi
* /tasks:
* post:
* summary: Tạo task mới
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateTask'
* responses:
* 201:
* description: Task đã tạo
* 400:
* description: Validation error
* 409:
* description: Task trùng title
*/
router.post("/", validate(createTaskSchema), controller.create);
/**
* @openapi
* /tasks/{id}:
* put:
* summary: Update task
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/CreateTask'
* responses:
* 200:
* description: Task đã update
* 404:
* description: Task không tìm thấy
*/
router.put("/:id", validate(updateTaskSchema), controller.update);
/**
* @openapi
* /tasks/{id}:
* delete:
* summary: Xóa task
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* format: uuid
* responses:
* 204:
* description: Task đã xóa
* 404:
* description: Task không tìm thấy
*/
router.delete("/:id", validate(getTaskSchema), controller.delete);
export default router;Mount Swagger UI trong app:
// Thêm vào src/app.ts
import swaggerUi from "swagger-ui-express";
import { swaggerSpec } from "@/config/swagger.js";
// Sau middleware khác, trước routes
app.use("/api/docs", swaggerUi.serve, swaggerUi.setup(swaggerSpec));Giờ truy cập http://localhost:3000/api/docs để xem interactive API documentation.
13. Tổng Quan Kiến Trúc
Đây là cách các layers kết nối với nhau:
Data chảy xuống: Request → Middleware → Route → Controller → Service → Repository → Database
Errors chảy lên: Bất kỳ layer nào throw → Error Handler bắt → JSON response nhất quán
Mỗi layer có một nhiệm vụ:
- Middleware: Cross-cutting concerns (auth, logging, validation)
- Controller: HTTP vào/ra
- Service: Business rules
- Repository: Data access
Tổng Kết
Đây là những gì chúng ta đã build và patterns đã dùng:
| Layer | Trách nhiệm | Files |
|---|---|---|
| Schemas | Validation + Types | schemas/task.schema.ts |
| Middleware | Cross-cutting concerns | middleware/*.ts |
| Routes | Định nghĩa endpoints | routes/task.routes.ts |
| Controllers | Xử lý HTTP | controllers/task.controller.ts |
| Services | Business logic | services/task.service.ts |
| Repositories | Data access | repositories/task.repository.ts |
| Errors | Error types nhất quán | utils/errors.ts |
Key takeaways:
- Zod cho bạn validation và types từ single source of truth
- Layered architecture giúp mỗi phần testable và replaceable
- Custom error classes cho consistent, predictable API responses
- Middleware order matters — parse body trước logging, handle errors cuối cùng
- API versioning với URL prefixes (
/api/v1/) đơn giản và hiệu quả
Tiếp Theo
Trong bài tiếp, chúng ta sẽ connect API này với database thực sự:
- Prisma ORM setup và schema design
- Migrations workflow
- CRUD với full type safety
- Transactions và connection pooling
- Testing database code
Series: TypeScript Full-Stack Development
Trước: TypeScript: Thiết Lập Project cho Node.js Backend
Tiếp: TypeScript: Tích Hợp Database với Express.js và Prisma
📬 Đă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.