跳至主要內容
Skip to content

實戰專案:完整後端 API 服務

本篇將整合前面學到的所有知識,打造一個生產級的 RESTful API 服務。


一、 專案結構

1.1 目錄規劃

src/
├── config/           # 配置
│   ├── index.js
│   └── database.js
├── middleware/       # 中介軟體
│   ├── auth.js
│   ├── error.js
│   ├── validation.js
│   └── rateLimit.js
├── routes/           # 路由
│   ├── index.js
│   ├── auth.js
│   └── users.js
├── controllers/      # 控制器
│   ├── auth.controller.js
│   └── user.controller.js
├── services/         # 業務邏輯
│   ├── auth.service.js
│   └── user.service.js
├── models/           # 資料模型
│   └── user.model.js
├── utils/            # 工具函數
│   ├── jwt.js
│   ├── password.js
│   └── response.js
├── validators/       # 驗證規則
│   ├── auth.validator.js
│   └── user.validator.js
└── app.js            # 入口

二、 基礎設置

2.1 依賴安裝

bash
npm init -y
npm install express cors helmet morgan compression
npm install jsonwebtoken bcryptjs joi
npm install express-rate-limit
npm install dotenv
npm install -D nodemon

2.2 配置檔

javascript
// config/index.js
require("dotenv").config();

module.exports = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || "development",
  jwt: {
    accessSecret: process.env.JWT_ACCESS_SECRET,
    refreshSecret: process.env.JWT_REFRESH_SECRET,
    accessExpiresIn: "15m",
    refreshExpiresIn: "7d",
  },
  cors: {
    origin: process.env.CORS_ORIGIN?.split(",") || ["http://localhost:3000"],
    credentials: true,
  },
};

2.3 應用入口

javascript
// app.js
const express = require("express");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
const compression = require("compression");

const config = require("./config");
const routes = require("./routes");
const errorHandler = require("./middleware/error");

const app = express();

// 安全標頭
app.use(helmet());

// 壓縮
app.use(compression());

// 日誌
if (config.nodeEnv !== "test") {
  app.use(morgan("combined"));
}

// CORS
app.use(cors(config.cors));

// Body 解析
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ extended: true }));

// 路由
app.use("/api", routes);

// 健康檢查
app.get("/health", (req, res) => {
  res.json({ status: "ok", timestamp: new Date().toISOString() });
});

// 404
app.use((req, res) => {
  res.status(404).json({
    error: { code: "NOT_FOUND", message: "Endpoint not found" },
  });
});

// 錯誤處理
app.use(errorHandler);

// 啟動
if (require.main === module) {
  app.listen(config.port, () => {
    console.log(`Server running on port ${config.port}`);
  });
}

module.exports = app;

三、 認證系統

3.1 JWT 工具

javascript
// utils/jwt.js
const jwt = require("jsonwebtoken");
const config = require("../config");

function generateAccessToken(payload) {
  return jwt.sign(payload, config.jwt.accessSecret, {
    expiresIn: config.jwt.accessExpiresIn,
  });
}

function generateRefreshToken(payload) {
  return jwt.sign(payload, config.jwt.refreshSecret, {
    expiresIn: config.jwt.refreshExpiresIn,
  });
}

function verifyAccessToken(token) {
  return jwt.verify(token, config.jwt.accessSecret);
}

function verifyRefreshToken(token) {
  return jwt.verify(token, config.jwt.refreshSecret);
}

function generateTokenPair(user) {
  const payload = { userId: user.id, email: user.email };
  return {
    accessToken: generateAccessToken(payload),
    refreshToken: generateRefreshToken({ userId: user.id }),
  };
}

module.exports = {
  generateAccessToken,
  generateRefreshToken,
  verifyAccessToken,
  verifyRefreshToken,
  generateTokenPair,
};

3.2 密碼處理

javascript
// utils/password.js
const bcrypt = require("bcryptjs");

const SALT_ROUNDS = 12;

async function hashPassword(password) {
  return bcrypt.hash(password, SALT_ROUNDS);
}

async function comparePassword(password, hash) {
  return bcrypt.compare(password, hash);
}

module.exports = { hashPassword, comparePassword };

3.3 認證中介軟體

javascript
// middleware/auth.js
const { verifyAccessToken } = require("../utils/jwt");
const { ApiError } = require("../utils/response");

function authenticate(req, res, next) {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith("Bearer ")) {
    throw new ApiError(401, "UNAUTHORIZED", "No token provided");
  }

  const token = authHeader.substring(7);

  try {
    const decoded = verifyAccessToken(token);
    req.user = decoded;
    next();
  } catch (err) {
    if (err.name === "TokenExpiredError") {
      throw new ApiError(401, "TOKEN_EXPIRED", "Token has expired");
    }
    throw new ApiError(401, "INVALID_TOKEN", "Invalid token");
  }
}

function optionalAuth(req, res, next) {
  const authHeader = req.headers.authorization;

  if (authHeader?.startsWith("Bearer ")) {
    try {
      const token = authHeader.substring(7);
      req.user = verifyAccessToken(token);
    } catch {}
  }

  next();
}

module.exports = { authenticate, optionalAuth };

3.4 認證路由

javascript
// routes/auth.js
const router = require("express").Router();
const authController = require("../controllers/auth.controller");
const { validate } = require("../middleware/validation");
const { registerSchema, loginSchema } = require("../validators/auth.validator");

router.post("/register", validate(registerSchema), authController.register);
router.post("/login", validate(loginSchema), authController.login);
router.post("/refresh", authController.refresh);
router.post("/logout", authController.logout);

module.exports = router;

3.5 認證控制器

javascript
// controllers/auth.controller.js
const authService = require("../services/auth.service");
const { ApiError } = require("../utils/response");

async function register(req, res) {
  const user = await authService.register(req.body);
  res.status(201).json(user);
}

async function login(req, res) {
  const { email, password } = req.body;
  const result = await authService.login(email, password);

  // 設置 Refresh Token Cookie
  res.cookie("refreshToken", result.refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000,
    path: "/api/auth/refresh",
  });

  res.json({
    user: result.user,
    accessToken: result.accessToken,
  });
}

async function refresh(req, res) {
  const refreshToken = req.cookies.refreshToken;

  if (!refreshToken) {
    throw new ApiError(401, "NO_REFRESH_TOKEN", "No refresh token");
  }

  const result = await authService.refresh(refreshToken);

  res.cookie("refreshToken", result.refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === "production",
    sameSite: "strict",
    maxAge: 7 * 24 * 60 * 60 * 1000,
    path: "/api/auth/refresh",
  });

  res.json({ accessToken: result.accessToken });
}

async function logout(req, res) {
  res.clearCookie("refreshToken", { path: "/api/auth/refresh" });
  res.json({ success: true });
}

module.exports = { register, login, refresh, logout };

四、 錯誤處理

4.1 錯誤類別

javascript
// utils/response.js
class ApiError extends Error {
  constructor(statusCode, code, message, details = null) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;
    this.details = details;
    this.isOperational = true;
  }
}

function sendSuccess(res, data, statusCode = 200) {
  res.status(statusCode).json(data);
}

function sendError(res, error) {
  res.status(error.statusCode || 500).json({
    error: {
      code: error.code || "INTERNAL_ERROR",
      message: error.message,
      ...(error.details && { details: error.details }),
    },
  });
}

module.exports = { ApiError, sendSuccess, sendError };

4.2 錯誤處理中介軟體

javascript
// middleware/error.js
const { sendError } = require("../utils/response");

function errorHandler(err, req, res, next) {
  // 記錄錯誤
  console.error({
    error: err.message,
    stack: err.stack,
    url: req.url,
    method: req.method,
  });

  // 已知錯誤
  if (err.isOperational) {
    return sendError(res, err);
  }

  // Joi 驗證錯誤
  if (err.isJoi) {
    return res.status(422).json({
      error: {
        code: "VALIDATION_ERROR",
        message: "Validation failed",
        details: err.details.map((d) => ({
          field: d.path.join("."),
          message: d.message,
        })),
      },
    });
  }

  // 未知錯誤
  res.status(500).json({
    error: {
      code: "INTERNAL_ERROR",
      message:
        process.env.NODE_ENV === "production"
          ? "An unexpected error occurred"
          : err.message,
    },
  });
}

module.exports = errorHandler;

五、 驗證

5.1 Joi 驗證器

javascript
// validators/auth.validator.js
const Joi = require("joi");

const registerSchema = Joi.object({
  name: Joi.string().min(2).max(50).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required(),
});

const loginSchema = Joi.object({
  email: Joi.string().email().required(),
  password: Joi.string().required(),
});

module.exports = { registerSchema, loginSchema };

5.2 驗證中介軟體

javascript
// middleware/validation.js
function validate(schema) {
  return (req, res, next) => {
    const { error, value } = schema.validate(req.body, {
      abortEarly: false,
      stripUnknown: true,
    });

    if (error) {
      error.isJoi = true;
      return next(error);
    }

    req.body = value;
    next();
  };
}

module.exports = { validate };

六、 限流

javascript
// middleware/rateLimit.js
const rateLimit = require("express-rate-limit");

const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分鐘
  max: 100,
  message: {
    error: {
      code: "TOO_MANY_REQUESTS",
      message: "Too many requests, please try again later",
    },
  },
  standardHeaders: true,
  legacyHeaders: false,
});

const authLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1 小時
  max: 10,
  message: {
    error: {
      code: "TOO_MANY_ATTEMPTS",
      message: "Too many login attempts",
    },
  },
});

module.exports = { apiLimiter, authLimiter };

七、 完整路由

javascript
// routes/index.js
const router = require("express").Router();
const authRoutes = require("./auth");
const userRoutes = require("./users");
const { apiLimiter, authLimiter } = require("../middleware/rateLimit");
const { authenticate } = require("../middleware/auth");

// 全域限流
router.use(apiLimiter);

// 認證路由
router.use("/auth", authLimiter, authRoutes);

// 需要認證的路由
router.use("/users", authenticate, userRoutes);

module.exports = router;

八、 部署配置

8.1 環境變數

bash
# .env
NODE_ENV=production
PORT=3000

JWT_ACCESS_SECRET=your-super-secret-access-key
JWT_REFRESH_SECRET=your-super-secret-refresh-key

CORS_ORIGIN=https://yourapp.com,https://admin.yourapp.com

DATABASE_URL=mongodb://...

8.2 PM2 配置

javascript
// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: "api",
      script: "src/app.js",
      instances: "max",
      exec_mode: "cluster",
      env_production: {
        NODE_ENV: "production",
      },
    },
  ],
};

總結

功能實現
認證JWT + Refresh Token
安全Helmet + CORS + Rate Limit
驗證Joi
錯誤統一錯誤格式
日誌Morgan
壓縮Compression

> **生產檢查清單**:

  • ✅ HTTPS
  • ✅ 環境變數
  • ✅ 錯誤處理
  • ✅ 限流
  • ✅ 日誌
  • ✅ 監控

進階挑戰

  1. 添加 Redis 儲存 Refresh Token。
  2. 實作 API Key 認證方式。
  3. 添加 Swagger 文檔。

延伸閱讀與資源