作为一名长期奋战在一线的全栈开发者,我深知快速搭建后端服务的重要性。Node.js配合Express框架的组合,就像一把瑞士军刀——轻便、灵活且功能强大。今天我要分享的这套方案,已经在我经手的十几个中小型项目中得到了验证,特别适合需要快速迭代的业务场景。
Express之所以广受欢迎,关键在于它极低的学习曲线和高度模块化的设计。你不需要像Spring Boot那样处理复杂的配置,也不用像Django那样遵循严格的MVC模式。只需几行代码,一个功能完备的RESTful API服务就能跑起来。对于需要快速验证想法的创业团队或个人开发者来说,这简直是效率神器。
在开始之前,请确保你的系统已经安装:
提示:使用nvm管理Node.js版本可以避免权限问题,特别是在Linux/macOS系统上
打开终端,执行以下命令序列:
bash复制# 创建项目目录并初始化npm项目
mkdir express-api-demo && cd express-api-demo
npm init -y
# 安装核心依赖
npm install express mysql2 body-parser cors
# 开发依赖(按需安装)
npm install --save-dev nodemon eslint prettier
这里有个实用技巧:在package.json中添加start和dev脚本:
json复制"scripts": {
"start": "node index.js",
"dev": "nodemon index.js"
}
使用nodemon可以实现代码热更新,开发时运行npm run dev,每次保存文件都会自动重启服务。
创建index.js文件,构建最小可用服务:
javascript复制const express = require('express')
const app = express()
const port = 3000
// 中间件配置
app.use(require('cors')())
app.use(require('body-parser').json())
// 健康检查端点
app.get('/health', (req, res) => {
res.json({ status: 'UP', timestamp: new Date() })
})
// 错误处理中间件
app.use((err, req, res, next) => {
console.error(err.stack)
res.status(500).send('Something broke!')
})
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`)
})
这个基础架构包含了:
直接使用mysql2的连接池替代单连接:
javascript复制const mysql = require('mysql2/promise')
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'yourpassword',
database: 'demo_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
})
// 使用示例
async function queryUsers() {
const [rows] = await pool.query('SELECT * FROM users')
return rows
}
连接池的优势:
创建routes/users.js:
javascript复制const router = require('express').Router()
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')
// 注册接口
router.post('/register', async (req, res) => {
try {
const { username, email, password } = req.body
// 密码加密
const hashedPassword = await bcrypt.hash(password, 10)
// 数据库操作
const [result] = await pool.query(
'INSERT INTO users SET ?',
{ username, email, password: hashedPassword }
)
res.status(201).json({
id: result.insertId,
username,
email
})
} catch (err) {
console.error(err)
res.status(500).json({ error: 'Registration failed' })
}
})
// 登录接口
router.post('/login', async (req, res) => {
const { email, password } = req.body
const [users] = await pool.query(
'SELECT * FROM users WHERE email = ?',
[email]
)
if (users.length === 0 || !await bcrypt.compare(password, users[0].password)) {
return res.status(401).json({ error: 'Invalid credentials' })
}
const user = users[0]
const token = jwt.sign(
{ userId: user.id },
process.env.JWT_SECRET || 'your-secret-key',
{ expiresIn: '1h' }
)
res.json({ token })
})
module.exports = router
关键安全措施:
创建routes/posts.js:
javascript复制const router = require('express').Router()
const auth = require('../middleware/auth')
// 获取帖子列表(分页)
router.get('/', async (req, res) => {
const page = parseInt(req.query.page) || 1
const limit = parseInt(req.query.limit) || 10
const offset = (page - 1) * limit
const [posts] = await pool.query(
'SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?',
[limit, offset]
)
const [[{ count }]] = await pool.query(
'SELECT COUNT(*) AS count FROM posts'
)
res.json({
data: posts,
meta: {
page,
limit,
total: count,
totalPages: Math.ceil(count / limit)
}
})
})
// 创建帖子(需要认证)
router.post('/', auth, async (req, res) => {
const { title, content } = req.body
const [result] = await pool.query(
'INSERT INTO posts SET ?',
{
title,
content,
user_id: req.userId
}
)
res.status(201).json({
id: result.insertId,
title,
content
})
})
module.exports = router
高级功能实现:
javascript复制// 在index.js中动态加载路由
const fs = require('fs')
fs.readdirSync('./routes').forEach(file => {
const route = require(`./routes/${file}`)
app.use(`/api/${file.replace('.js', '')}`, route)
})
javascript复制const compression = require('compression')
app.use(compression())
javascript复制app.get('/static/*', (req, res, next) => {
res.set('Cache-Control', 'public, max-age=31536000')
next()
})
javascript复制module.exports = {
apps: [{
name: 'express-api',
script: 'index.js',
instances: 'max',
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'production',
PORT: 3000,
DATABASE_URL: 'mysql://user:pass@host:port/db'
}
}]
}
启动命令:
bash复制pm2 start ecosystem.config.js
nginx复制server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
症状:出现"ER_NOT_SUPPORTED_AUTH_MODE"错误
解决方案:
sql复制-- 在MySQL中执行
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'yourpassword';
FLUSH PRIVILEGES;
如果需要更精细的CORS控制:
javascript复制const corsOptions = {
origin: [
'https://yourdomain.com',
'http://localhost:8080'
],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}
app.use(cors(corsOptions))
使用clinic.js进行性能诊断:
bash复制npm install -g clinic
clinic doctor -- node index.js
# 进行压力测试
clinic flame -- node index.js
成熟的Express项目结构示例:
code复制/src
/config
database.js
auth.js
/controllers
userController.js
postController.js
/middlewares
auth.js
errorHandler.js
/models
User.js
Post.js
/routes
index.js
userRoutes.js
postRoutes.js
/services
userService.js
postService.js
/utils
logger.js
apiResponse.js
app.js
server.js
这种结构优势:
javascript复制const helmet = require('helmet')
const rateLimit = require('express-rate-limit')
// 基础安全防护
app.use(helmet())
// 请求限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100 // 每个IP限制100次请求
})
app.use(limiter)
// 防止XSS攻击
app.use(require('xss-clean')())
// 防止参数污染
app.use(require('hpp')())
始终使用参数化查询:
javascript复制// 错误示范(易受注入攻击)
const query = `SELECT * FROM users WHERE username = '${req.body.username}'`
// 正确做法
const query = 'SELECT * FROM users WHERE username = ?'
const [users] = await pool.query(query, [req.body.username])
使用dotenv管理环境变量:
bash复制npm install dotenv
创建.env文件:
code复制DB_HOST=localhost
DB_USER=root
DB_PASS=secret
JWT_SECRET=your-ultra-secure-key
在代码中引用:
javascript复制require('dotenv').config()
const pool = mysql.createPool({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASS
})
javascript复制const request = require('supertest')
const app = require('../app')
const pool = require('../config/database')
describe('User API', () => {
beforeAll(async () => {
await pool.query('TRUNCATE TABLE users')
})
it('should register a new user', async () => {
const res = await request(app)
.post('/api/users/register')
.send({
username: 'testuser',
email: 'test@example.com',
password: 'password123'
})
expect(res.statusCode).toEqual(201)
expect(res.body).toHaveProperty('id')
})
afterAll(async () => {
await pool.end()
})
})
在package.json中添加:
json复制"scripts": {
"test": "jest --coverage",
"test:watch": "jest --watch"
}
使用winston日志库:
javascript复制const { createLogger, format, transports } = require('winston')
const logger = createLogger({
level: 'info',
format: format.combine(
format.timestamp(),
format.json()
),
transports: [
new transports.File({ filename: 'error.log', level: 'error' }),
new transports.File({ filename: 'combined.log' }),
new transports.Console({
format: format.simple()
})
]
})
// 在中间件中使用
app.use((req, res, next) => {
logger.info(`${req.method} ${req.url}`)
next()
})
javascript复制const healthcheck = require('express-healthcheck')
app.use('/health', healthcheck({
healthy: async () => {
// 测试数据库连接
try {
const conn = await pool.getConnection()
conn.release()
return { db: 'up' }
} catch (err) {
throw new Error('DB connection failed')
}
}
}))
javascript复制const WebSocket = require('ws')
const server = app.listen(3000)
const wss = new WebSocket.Server({ server })
wss.on('connection', (ws) => {
console.log('New client connected')
ws.on('message', (message) => {
console.log(`Received: ${message}`)
wss.clients.forEach(client => {
if (client !== ws && client.readyState === WebSocket.OPEN) {
client.send(message)
}
})
})
})
使用Bull处理异步任务:
javascript复制const Queue = require('bull')
const emailQueue = new Queue('email', {
redis: {
host: 'redis-server',
port: 6379
}
})
// 生产者
emailQueue.add({
to: 'user@example.com',
subject: 'Welcome!'
})
// 消费者
emailQueue.process(async (job) => {
await sendEmail(job.data)
})
javascript复制const client = require('prom-client')
// 收集默认指标
client.collectDefaultMetrics()
// 自定义计数器
const httpRequestCounter = new client.Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status']
})
// 中间件记录请求
app.use((req, res, next) => {
res.on('finish', () => {
httpRequestCounter.labels(
req.method,
req.route.path,
res.statusCode
).inc()
})
next()
})
// 暴露指标端点
app.get('/metrics', async (req, res) => {
res.set('Content-Type', client.register.contentType)
res.end(await client.register.metrics())
})
使用Jaeger实现:
javascript复制const { initTracer } = require('jaeger-client')
const config = {
serviceName: 'express-api',
sampler: {
type: 'const',
param: 1
},
reporter: {
logSpans: true,
agentHost: 'jaeger-agent'
}
}
const tracer = initTracer(config)
app.get('/api/posts', (req, res) => {
const span = tracer.startSpan('get_posts')
// 业务逻辑...
span.finish()
res.json(posts)
})
创建.github/workflows/ci.yml:
yaml复制name: CI Pipeline
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: test_db
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v2
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Run tests
env:
DB_HOST: localhost
DB_USER: root
DB_PASS: root
DB_NAME: test_db
run: npm test
创建Dockerfile:
dockerfile复制FROM node:16-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
docker-compose.yml示例:
yaml复制version: '3'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DB_HOST=mysql
- DB_USER=root
- DB_PASS=secret
depends_on:
- mysql
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: app_db
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:
使用swagger-jsdoc:
javascript复制const swaggerJSDoc = require('swagger-jsdoc')
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'Express API',
version: '1.0.0'
}
},
apis: ['./routes/*.js']
}
const swaggerSpec = swaggerJSDoc(options)
app.use('/api-docs',
require('swagger-ui-express').serve,
require('swagger-ui-express').setup(swaggerSpec)
)
在路由中添加JSDoc注释:
javascript复制/**
* @swagger
* /api/users:
* post:
* summary: Register a new user
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* username:
* type: string
* email:
* type: string
* password:
* type: string
* responses:
* 201:
* description: User registered
*/
router.post('/users', userController.register)
使用multer中间件:
javascript复制const multer = require('multer')
const path = require('path')
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/')
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`)
}
})
const upload = multer({
storage,
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname)
if (['.jpg', '.png', '.gif'].includes(ext)) {
return cb(null, true)
}
cb(new Error('Only images are allowed'))
},
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
}
})
router.post('/upload', upload.single('avatar'), (req, res) => {
res.json({
url: `/uploads/${req.file.filename}`
})
})
使用Server-Sent Events:
javascript复制let clients = []
router.get('/notifications', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
clients.push(res)
req.on('close', () => {
clients = clients.filter(client => client !== res)
})
})
function sendNotification(message) {
clients.forEach(client => {
client.write(`data: ${JSON.stringify(message)}\n\n`)
})
}
// 在其他路由中调用
app.post('/posts', async (req, res) => {
// 创建帖子逻辑...
sendNotification({
type: 'new_post',
data: post
})
res.status(201).json(post)
})
当项目规模扩大时,可以考虑按业务域拆分:
code复制/services
/user-service
/src
Dockerfile
package.json
/post-service
/src
Dockerfile
package.json
/gateway
/src
Dockerfile
package.json
使用HTTP或gRPC:
javascript复制// 在post-service中调用user-service
const axios = require('axios')
async function getUser(userId) {
try {
const response = await axios.get(
`http://user-service/api/users/${userId}`
)
return response.data
} catch (err) {
console.error('User service error:', err.message)
throw err
}
}
使用服务发现:
javascript复制const consul = require('consul')()
async function getServiceUrl(serviceName) {
const services = await consul.agent.service.list()
const service = Object.values(services)
.find(s => s.Service === serviceName)
if (!service) throw new Error(`Service ${serviceName} not found`)
return `http://${service.Address}:${service.Port}`
}
创建baseRepository.js:
javascript复制class BaseRepository {
constructor(tableName) {
this.table = tableName
}
async findById(id) {
const [rows] = await pool.query(
`SELECT * FROM ${this.table} WHERE id = ?`,
[id]
)
return rows[0] || null
}
async create(data) {
const [result] = await pool.query(
`INSERT INTO ${this.table} SET ?`,
[data]
)
return this.findById(result.insertId)
}
// 其他通用方法...
}
module.exports = BaseRepository
用户仓库继承:
javascript复制const BaseRepository = require('./baseRepository')
class UserRepository extends BaseRepository {
constructor() {
super('users')
}
async findByEmail(email) {
const [rows] = await pool.query(
'SELECT * FROM users WHERE email = ?',
[email]
)
return rows[0] || null
}
}
创建userDto.js:
javascript复制class UserDto {
constructor(user) {
this.id = user.id
this.username = user.username
this.email = user.email
this.createdAt = user.created_at
}
toJSON() {
return {
id: this.id,
username: this.username,
email: this.email,
createdAt: this.createdAt.toISOString()
}
}
}
module.exports = UserDto
在控制器中使用:
javascript复制const UserDto = require('../dtos/userDto')
router.get('/:id', async (req, res) => {
const user = await userRepository.findById(req.params.id)
if (!user) return res.status(404).send()
res.json(new UserDto(user))
})
安装视图引擎:
bash复制npm install ejs
配置Express:
javascript复制app.set('views', path.join(__dirname, 'views'))
app.set('view engine', 'ejs')
app.get('/', (req, res) => {
res.render('index', {
title: 'Home Page',
posts: await postService.getLatest()
})
})
三种常见方案:
javascript复制app.use('/api/v1', require('./routes/v1'))
app.use('/api/v2', require('./routes/v2'))
javascript复制app.use('/api', (req, res, next) => {
const version = req.headers['x-api-version'] || 'v1'
require(`./routes/${version}`)(req, res, next)
})
javascript复制app.use('/api', (req, res, next) => {
const accept = req.headers.accept || ''
const version = accept.includes('vnd.myapp.v2+json') ? 'v2' : 'v1'
require(`./routes/${version}`)(req, res, next)
})
使用i18n中间件:
javascript复制const i18n = require('i18n')
i18n.configure({
locales: ['en', 'zh'],
directory: path.join(__dirname, 'locales'),
defaultLocale: 'en',
cookie: 'lang'
})
app.use(i18n.init)
// 在路由中使用
router.get('/greeting', (req, res) => {
res.send(res.__('Hello World'))
})
locales/en.json:
json复制{
"Hello World": "Hello World",
"User not found": "User not found"
}
locales/zh.json:
json复制{
"Hello World": "你好世界",
"User not found": "用户未找到"
}
使用moment-timezone:
javascript复制const moment = require('moment-timezone')
router.get('/time', (req, res) => {
const tz = req.query.tz || 'Asia/Shanghai'
res.json({
time: moment().tz(tz).format('YYYY-MM-DD HH:mm:ss')
})
})
经过这个完整项目的实践,你应该已经掌握了使用Express构建生产级API服务的核心技能。以下是我在实际项目中的几点经验总结:
目录结构规划:前期合理的项目结构设计比后期重构要轻松得多。建议参考Node.js最佳实践的项目布局。
错误处理统一化:尽早建立全局错误处理机制,使用自定义错误类区分业务错误和系统错误。
配置管理:将不同环境的配置分离,使用dotenv加载环境变量,避免硬编码敏感信息。
性能监控:在生产环境部署APM工具(如New Relic或AppDynamics),建立性能基线。
文档自动化:将API文档作为开发流程的一部分,可以考虑使用OpenAPI规范。
安全审计:定期进行依赖项安全检查(npm audit),设置CI流水线中的安全扫描步骤。
对于想要进一步深入的学习者,我推荐以下方向:
记住,框架只是工具,核心还是对HTTP协议、Web标准和软件设计原则的理解。Express的简洁设计恰恰为我们提供了学习这些核心概念的绝佳平台。