Node.js

Node.js 後端開發實戰指南

從零開始建立Node.js後端服務,包含Express框架、資料庫整合、API設計、身份驗證和部署等實用技巧。

陳工程師
25分鐘閱讀
#Node.js#Express#MongoDB#後端開發

# 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後端專案的基礎,根據具體需求進行擴展和修改。
Node.js 後端開發實戰指南 | 香港大專CS功課代做