บทที่ 8backend|11 KB
บทที่ 8: Middleware และ Error Handling
สารบัญเนื้อหา
บทที่ 8: Middleware และ Error Handling
🎯 เป้าหมายการเรียนรู้
หลังจากเรียนบทนี้แล้ว คุณจะสามารถ:
- เข้าใจ Middleware Pattern
- สร้าง Custom Middleware
- จัดการ Errors อย่างเป็นระบบ
- ใช้ Error Handler Middleware
- Logging และ Monitoring
📚 ทฤษฎีและแนวคิด
Middleware คืออะไร?
Middleware คือ function ที่ทำงานระหว่าง request และ response
Request
↓
Middleware 1 (CORS)
↓
Middleware 2 (Auth)
↓
Middleware 3 (Logging)
↓
Route Handler
↓
Response
Middleware Signature
function middleware(req, res, next) {
// Do something
next(); // Pass to next middleware
}
Parameters:
req: Request objectres: Response objectnext: Function to call next middleware
💻 โค้ดจริงจาก QUOM App
1. Auth Middleware (ทบทวน)
ไฟล์: backend/src/middleware/auth.js
import jwt from "jsonwebtoken";
export function requireAuth(req, res, next) {
try {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
ok: false,
message: "No token provided"
});
}
const token = authHeader.startsWith("Bearer ")
? authHeader.slice(7)
: authHeader;
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded; // Attach user to request
next(); // Continue to next middleware/route
} catch (error) {
if (error.name === "TokenExpiredError") {
return res.status(401).json({
ok: false,
message: "Token expired"
});
}
return res.status(401).json({
ok: false,
message: "Invalid token"
});
}
}
อธิบายทีละบรรทัด (Imports):
import jwt from "jsonwebtoken"- ใช้สำหรับแกะ (Verify) Token ที่ส่งมากับ Header
การใช้งาน:
// Apply to single route
app.get("/api/profile", requireAuth, (req, res) => {
res.json({ user: req.user });
});
// Apply to all routes in router
notesRouter.use(requireAuth);
2. Admin Middleware
export function requireAdmin(req, res, next) {
if (!req.user) {
return res.status(401).json({
ok: false,
message: "Not authenticated"
});
}
if (req.user.role !== "admin") {
return res.status(403).json({
ok: false,
message: "Admin access required"
});
}
next();
}
การใช้งาน:
// Chain middlewares
app.get("/api/admin/users",
requireAuth, // First: Check auth
requireAdmin, // Then: Check admin
async (req, res) => {
const users = await User.find();
res.json({ users });
}
);
3. Logging Middleware
export function requestLogger(req, res, next) {
const start = Date.now();
// Log when response finishes
res.on("finish", () => {
const duration = Date.now() - start;
console.log(
`${req.method} ${req.path} ${res.statusCode} - ${duration}ms`
);
});
next();
}
Output:
GET /api/notes 200 - 45ms
POST /api/auth/login 401 - 123ms
GET /api/notes/123 404 - 12ms
4. CORS Middleware
ไฟล์: backend/src/server.js
import cors from "cors";
app.use(cors({
origin: process.env.CORS_ORIGIN || "*",
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"]
}));
Environment Variable:
CORS_ORIGIN=https://quom.app,http://localhost:3000
5. Error Handler Middleware
ไฟล์: backend/src/middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
console.error("❌ Error:", err);
// Mongoose validation error
if (err.name === "ValidationError") {
return res.status(400).json({
ok: false,
message: "Validation failed",
errors: Object.values(err.errors).map(e => e.message)
});
}
// Mongoose duplicate key error
if (err.code === 11000) {
const field = Object.keys(err.keyPattern)[0];
return res.status(409).json({
ok: false,
message: `${field} already exists`
});
}
// Zod validation error
if (err.name === "ZodError") {
return res.status(400).json({
ok: false,
message: "Validation failed",
errors: err.errors.map(e => ({
field: e.path.join("."),
message: e.message
}))
});
}
// JWT errors
if (err.name === "JsonWebTokenError") {
return res.status(401).json({
ok: false,
message: "Invalid token"
});
}
if (err.name === "TokenExpiredError") {
return res.status(401).json({
ok: false,
message: "Token expired"
});
}
// Default error
res.status(err.status || 500).json({
ok: false,
message: err.message || "Internal server error"
});
}
การใช้งาน:
// ต้องอยู่ท้ายสุด (หลัง routes ทั้งหมด)
app.use(errorHandler);
6. Async Error Wrapper
export function asyncHandler(fn) {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
การใช้งาน:
// Without wrapper - ต้อง try-catch เอง
app.get("/api/notes", async (req, res) => {
try {
const notes = await Note.find();
res.json({ notes });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// With wrapper - errors จะถูกส่งไป errorHandler อัตโนมัติ
app.get("/api/notes", asyncHandler(async (req, res) => {
const notes = await Note.find();
res.json({ notes });
}));
7. Request Validation Middleware
import { z } from "zod";
export function validate(schema) {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
ok: false,
message: "Validation failed",
errors: error.errors.map(e => ({
field: e.path.join("."),
message: e.message
}))
});
}
next(error);
}
};
}
การใช้งาน:
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6)
});
app.post("/api/auth/login",
validate(loginSchema),
async (req, res) => {
// req.body is validated
const { email, password } = req.body;
// ...
}
);
8. Rate Limiting Middleware
const rateLimit = new Map(); // IP -> { count, resetAt }
export function rateLimiter(maxRequests = 100, windowMs = 60000) {
return (req, res, next) => {
const ip = req.ip;
const now = Date.now();
let record = rateLimit.get(ip);
if (!record || now > record.resetAt) {
record = { count: 0, resetAt: now + windowMs };
rateLimit.set(ip, record);
}
record.count++;
if (record.count > maxRequests) {
return res.status(429).json({
ok: false,
message: "Too many requests"
});
}
next();
};
}
การใช้งาน:
// Apply globally
app.use(rateLimiter(100, 60000)); // 100 requests per minute
// Apply to specific routes
app.post("/api/auth/login",
rateLimiter(5, 60000), // 5 login attempts per minute
async (req, res) => {
// ...
}
);
🔄 Middleware Order
สำคัญมาก! ลำดับของ middleware มีผล:
import express from "express";
import cors from "cors";
import morgan from "morgan";
const app = express();
// 1. CORS (ต้องมาก่อน routes)
app.use(cors());
// 2. Body parsers
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// 3. Logging
app.use(morgan("dev"));
// 4. Custom middleware
app.use(requestLogger);
// 5. Routes
app.use("/api/auth", authRouter);
app.use("/api/notes", notesRouter);
// 6. 404 Handler
app.use((req, res) => {
res.status(404).json({ ok: false, message: "Not found" });
});
// 7. Error Handler (ต้องอยู่ท้ายสุด!)
app.use(errorHandler);
🔒 Security Middleware
1. Helmet (Security Headers)
import helmet from "helmet";
app.use(helmet());
Headers ที่เพิ่ม:
X-Content-Type-Options: nosniffX-Frame-Options: DENYX-XSS-Protection: 1; mode=block
2. Express Rate Limit
import rateLimit from "express-rate-limit";
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per windowMs
});
app.use("/api/", limiter);
3. HPP (HTTP Parameter Pollution)
import hpp from "hpp";
app.use(hpp());
📝 แบบฝึกหัด
โจทย์ที่ 1: API Key Middleware
// TODO: สร้าง middleware ตรวจสอบ API key
// - อ่านจาก header X-API-Key
// - เปรียบเทียบกับ process.env.API_KEY
โจทย์ที่ 2: Request ID Middleware
// TODO: เพิ่ม unique request ID
// - สร้าง UUID
// - เพิ่มใน req.id
// - Log ทุก request พร้อม ID
โจทย์ที่ 3: Cache Middleware
// TODO: Cache GET requests
// - เก็บ response ใน memory
// - Return cached ถ้ามี
// - TTL 5 minutes
📌 สรุป
✅ Middleware Pattern - Request → Middleware → Response
✅ Auth Middleware - ตรวจสอบ JWT token
✅ Error Handler - จัดการ errors แบบรวมศูนย์
✅ Validation - Validate input ด้วย Zod
✅ Rate Limiting - ป้องกัน abuse
✅ Security - Helmet, CORS, HPP
ไฟล์ที่เกี่ยวข้อง
🔜 บทถัดไป
บทที่ 13: Local Storage (SharedPreferences)
เริ่มส่วน Mobile Development ต่อ!