實戰專案:完整後端 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 nodemon2.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
- ✅ 環境變數
- ✅ 錯誤處理
- ✅ 限流
- ✅ 日誌
- ✅ 監控
進階挑戰
- 添加 Redis 儲存 Refresh Token。
- 實作 API Key 認證方式。
- 添加 Swagger 文檔。