# Node.js 後端開發實戰指南
Node.js已經成為現代後端開發的主流技術之一。本文將帶你從零開始建立一個完整的Node.js後端服務。
## 1. 專案初始化與環境設置
### 建立專案結構
bash
mkdir my-nodejs-api
cd my-nodejs-api
npm init -y
### 安裝必要依賴
bash
npm install express cors helmet morgan dotenv mongoose bcryptjs jsonwebtoken
npm install --save-dev nodemon
### 基本專案結構
my-nodejs-api/
├── src/
│ ├── controllers/
│ ├── models/
│ ├── routes/
│ ├── middleware/
│ ├── utils/
│ └── app.js
├── .env
├── package.json
└── README.md
## 2. Express 應用程式設置
### 基本Express應用程式
javascript
// src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3000;
// 中間件
app.use(helmet()); // 安全標頭
app.use(cors()); // CORS支援
app.use(morgan('combined')); // 日誌記錄
app.use(express.json()); // JSON解析
app.use(express.urlencoded({ extended: true }));
// 路由
app.get('/', (req, res) => {
res.json({ message: '歡迎使用 Node.js API' });
});
// 錯誤處理中間件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: '伺服器內部錯誤' });
});
// 404處理
app.use('*', (req, res) => {
res.status(404).json({ error: '路由不存在' });
});
app.listen(PORT, () => {
console.log(`伺服器運行在 http://localhost:${PORT}`);
});
module.exports = app;
## 3. 資料庫整合 (MongoDB)
### 資料庫連接
javascript
// src/config/database.js
const mongoose = require('mongoose');
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGODB_URI, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log(`MongoDB 連接成功: ${conn.connection.host}`);
} catch (error) {
console.error('資料庫連接失敗:', error);
process.exit(1);
}
};
module.exports = connectDB;
### 使用者模型
javascript
// src/models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true,
minlength: 3
},
email: {
type: String,
required: true,
unique: true,
lowercase: true,
match: [/^w+([.-]?w+)*@w+([.-]?w+)*(.w{2,3})+$/, '請輸入有效的電子郵件']
},
password: {
type: String,
required: true,
minlength: 6
},
role: {
type: String,
enum: ['user', 'admin'],
default: 'user'
},
isActive: {
type: Boolean,
default: true
}
}, {
timestamps: true
});
// 密碼加密中間件
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
try {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
} catch (error) {
next(error);
}
});
// 密碼驗證方法
userSchema.methods.comparePassword = async function(candidatePassword) {
return bcrypt.compare(candidatePassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
### 文章模型
javascript
// src/models/Post.js
const mongoose = require('mongoose');
const postSchema = new mongoose.Schema({
title: {
type: String,
required: true,
trim: true,
maxlength: 200
},
content: {
type: String,
required: true
},
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
tags: [{
type: String,
trim: true
}],
status: {
type: String,
enum: ['draft', 'published'],
default: 'draft'
},
publishedAt: {
type: Date
}
}, {
timestamps: true
});
// 索引
postSchema.index({ title: 'text', content: 'text' });
postSchema.index({ author: 1, createdAt: -1 });
module.exports = mongoose.model('Post', postSchema);
## 4. 身份驗證與授權
### JWT工具函數
javascript
// src/utils/jwt.js
const jwt = require('jsonwebtoken');
const generateToken = (userId) => {
return jwt.sign(
{ userId },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
};
const verifyToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('無效的token');
}
};
module.exports = { generateToken, verifyToken };
### 身份驗證中間件
javascript
// src/middleware/auth.js
const { verifyToken } = require('../utils/jwt');
const User = require('../models/User');
const authenticateToken = async (req, res, next) => {
try {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: '缺少認證token' });
}
const decoded = verifyToken(token);
const user = await User.findById(decoded.userId).select('-password');
if (!user) {
return res.status(401).json({ error: '用戶不存在' });
}
req.user = user;
next();
} catch (error) {
return res.status(403).json({ error: '無效的token' });
}
};
const authorizeRoles = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: '權限不足'
});
}
next();
};
};
module.exports = { authenticateToken, authorizeRoles };
## 5. 控制器層
### 使用者控制器
javascript
// src/controllers/userController.js
const User = require('../models/User');
const { generateToken } = require('../utils/jwt');
// 註冊
const register = async (req, res) => {
try {
const { username, email, password } = req.body;
// 檢查用戶是否已存在
const existingUser = await User.findOne({
$or: [{ email }, { username }]
});
if (existingUser) {
return res.status(400).json({
error: '用戶名或電子郵件已存在'
});
}
// 創建新用戶
const user = new User({
username,
email,
password
});
await user.save();
// 生成token
const token = generateToken(user._id);
res.status(201).json({
message: '註冊成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
error: '註冊失敗',
details: error.message
});
}
};
// 登入
const login = async (req, res) => {
try {
const { email, password } = req.body;
// 查找用戶
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({
error: '電子郵件或密碼錯誤'
});
}
// 驗證密碼
const isPasswordValid = await user.comparePassword(password);
if (!isPasswordValid) {
return res.status(401).json({
error: '電子郵件或密碼錯誤'
});
}
// 生成token
const token = generateToken(user._id);
res.json({
message: '登入成功',
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
res.status(500).json({
error: '登入失敗',
details: error.message
});
}
};
// 獲取用戶資料
const getProfile = async (req, res) => {
try {
res.json({
user: req.user
});
} catch (error) {
res.status(500).json({
error: '獲取用戶資料失敗'
});
}
};
module.exports = {
register,
login,
getProfile
};
### 文章控制器
javascript
// src/controllers/postController.js
const Post = require('../models/Post');
// 創建文章
const createPost = async (req, res) => {
try {
const { title, content, tags, status } = req.body;
const post = new Post({
title,
content,
tags,
status,
author: req.user._id,
publishedAt: status === 'published' ? new Date() : null
});
await post.save();
await post.populate('author', 'username');
res.status(201).json({
message: '文章創建成功',
post
});
} catch (error) {
res.status(500).json({
error: '創建文章失敗',
details: error.message
});
}
};
// 獲取所有文章
const getPosts = async (req, res) => {
try {
const { page = 1, limit = 10, status, author, search } = req.query;
const query = {};
if (status) query.status = status;
if (author) query.author = author;
if (search) {
query.$text = { $search: search };
}
const posts = await Post.find(query)
.populate('author', 'username')
.sort({ createdAt: -1 })
.limit(limit * 1)
.skip((page - 1) * limit);
const total = await Post.countDocuments(query);
res.json({
posts,
totalPages: Math.ceil(total / limit),
currentPage: page,
total
});
} catch (error) {
res.status(500).json({
error: '獲取文章失敗',
details: error.message
});
}
};
// 獲取單篇文章
const getPost = async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate('author', 'username');
if (!post) {
return res.status(404).json({
error: '文章不存在'
});
}
res.json({ post });
} catch (error) {
res.status(500).json({
error: '獲取文章失敗',
details: error.message
});
}
};
// 更新文章
const updatePost = async (req, res) => {
try {
const { title, content, tags, status } = req.body;
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: '文章不存在'
});
}
// 檢查權限
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({
error: '無權限修改此文章'
});
}
post.title = title || post.title;
post.content = content || post.content;
post.tags = tags || post.tags;
post.status = status || post.status;
if (status === 'published' && post.status !== 'published') {
post.publishedAt = new Date();
}
await post.save();
await post.populate('author', 'username');
res.json({
message: '文章更新成功',
post
});
} catch (error) {
res.status(500).json({
error: '更新文章失敗',
details: error.message
});
}
};
// 刪除文章
const deletePost = async (req, res) => {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
error: '文章不存在'
});
}
// 檢查權限
if (post.author.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({
error: '無權限刪除此文章'
});
}
await Post.findByIdAndDelete(req.params.id);
res.json({
message: '文章刪除成功'
});
} catch (error) {
res.status(500).json({
error: '刪除文章失敗',
details: error.message
});
}
};
module.exports = {
createPost,
getPosts,
getPost,
updatePost,
deletePost
};
## 6. 路由設置
### 使用者路由
javascript
// src/routes/userRoutes.js
const express = require('express');
const { register, login, getProfile } = require('../controllers/userController');
const { authenticateToken } = require('../middleware/auth');
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
router.get('/profile', authenticateToken, getProfile);
module.exports = router;
### 文章路由
javascript
// src/routes/postRoutes.js
const express = require('express');
const {
createPost,
getPosts,
getPost,
updatePost,
deletePost
} = require('../controllers/postController');
const { authenticateToken, authorizeRoles } = require('../middleware/auth');
const router = express.Router();
// 公開路由
router.get('/', getPosts);
router.get('/:id', getPost);
// 需要認證的路由
router.post('/', authenticateToken, createPost);
router.put('/:id', authenticateToken, updatePost);
router.delete('/:id', authenticateToken, deletePost);
module.exports = router;
### 主路由
javascript
// src/routes/index.js
const express = require('express');
const userRoutes = require('./userRoutes');
const postRoutes = require('./postRoutes');
const router = express.Router();
router.use('/api/users', userRoutes);
router.use('/api/posts', postRoutes);
module.exports = router;
## 7. 環境變數配置
bash
# .env
PORT=3000
MONGODB_URI=mongodb://localhost:27017/my-nodejs-api
JWT_SECRET=your-super-secret-jwt-key
NODE_ENV=development
## 8. 錯誤處理與驗證
### 自定義錯誤類別
javascript
// src/utils/errors.js
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
### 請求驗證中間件
javascript
// src/middleware/validation.js
const { validationResult } = require('express-validator');
const validateRequest = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: '請求驗證失敗',
details: errors.array()
});
}
next();
};
module.exports = validateRequest;
## 9. 測試設置
### 基本測試
javascript
// tests/user.test.js
const request = require('supertest');
const app = require('../src/app');
const User = require('../src/models/User');
describe('User API', () => {
beforeEach(async () => {
await User.deleteMany({});
});
describe('POST /api/users/register', () => {
it('應該成功註冊新用戶', async () => {
const userData = {
username: 'testuser',
email: 'test@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users/register')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('token');
expect(response.body.user.username).toBe(userData.username);
});
});
});
## 10. 部署準備
### PM2配置
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'my-nodejs-api',
script: './src/app.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development'
},
env_production: {
NODE_ENV: 'production'
}
}]
};
### Docker配置
dockerfile
# Dockerfile
FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
## 總結
這個Node.js後端開發指南涵蓋了:
1. **專案結構設置**:清晰的目錄組織
2. **Express應用程式**:基本設置和中間件
3. **資料庫整合**:MongoDB和Mongoose
4. **身份驗證**:JWT和密碼加密
5. **API設計**:RESTful API設計模式
6. **錯誤處理**:統一的錯誤處理機制
7. **測試**:基本的API測試
8. **部署**:生產環境部署準備
這個架構可以作為大多數Node.js後端專案的基礎,根據具體需求進行擴展和修改。