บทที่ 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 object
  • res: Response object
  • next: 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: nosniff
  • X-Frame-Options: DENY
  • X-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 ต่อ!


📚 แหล่งเรียนรู้เพิ่มเติม