RESTful API #

一、项目概述 #

1.1 项目结构 #

text
api-project/
├── config/
│   ├── database.js
│   └── index.js
├── controllers/
│   ├── authController.js
│   ├── userController.js
│   └── postController.js
├── middlewares/
│   ├── auth.js
│   ├── errorHandler.js
│   └── validate.js
├── models/
│   ├── User.js
│   └── Post.js
├── routes/
│   ├── index.js
│   ├── authRoutes.js
│   ├── userRoutes.js
│   └── postRoutes.js
├── services/
│   ├── authService.js
│   └── postService.js
├── utils/
│   ├── apiResponse.js
│   ├── asyncHandler.js
│   └── jwt.js
├── tests/
├── app.js
├── server.js
└── package.json

二、入口文件 #

2.1 app.js #

javascript
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const morgan = require('morgan');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');

const routes = require('./routes');
const errorHandler = require('./middlewares/errorHandler');

const app = express();

app.use(helmet());

app.use(cors({
    origin: process.env.CORS_ORIGIN || '*',
    credentials: true
}));

if (process.env.NODE_ENV === 'development') {
    app.use(morgan('dev'));
}

const limiter = rateLimit({
    windowMs: 15 * 60 * 1000,
    max: 100
});
app.use('/api', limiter);

app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true }));

app.use(mongoSanitize());
app.use(xss());

app.use('/api', routes);

app.use(errorHandler);

module.exports = app;

2.2 server.js #

javascript
const dotenv = require('dotenv');
dotenv.config();

const app = require('./app');
const connectDB = require('./config/database');

connectDB();

const PORT = process.env.PORT || 3000;

const server = app.listen(PORT, () => {
    console.log(`服务器运行在端口 ${PORT}`);
});

process.on('unhandledRejection', (err) => {
    console.error('未处理的Promise拒绝:', err.message);
    server.close(() => process.exit(1));
});

三、数据库配置 #

3.1 config/database.js #

javascript
const mongoose = require('mongoose');

const connectDB = async () => {
    try {
        const conn = await mongoose.connect(process.env.DATABASE_URL, {
            maxPoolSize: 10,
            serverSelectionTimeoutMS: 5000
        });
        console.log(`数据库连接成功: ${conn.connection.host}`);
    } catch (error) {
        console.error('数据库连接失败:', error.message);
        process.exit(1);
    }
};

module.exports = connectDB;

四、模型定义 #

4.1 models/User.js #

javascript
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
    name: {
        type: String,
        required: [true, '请输入用户名'],
        trim: true,
        maxlength: [50, '用户名不能超过50个字符']
    },
    email: {
        type: String,
        required: [true, '请输入邮箱'],
        unique: true,
        lowercase: true,
        match: [/^\S+@\S+\.\S+$/, '请输入有效的邮箱地址']
    },
    password: {
        type: String,
        required: [true, '请输入密码'],
        minlength: [6, '密码至少6个字符'],
        select: false
    },
    role: {
        type: String,
        enum: ['user', 'admin'],
        default: 'user'
    },
    avatar: String,
    bio: String
}, {
    timestamps: true
});

userSchema.pre('save', async function(next) {
    if (!this.isModified('password')) return next();
    this.password = await bcrypt.hash(this.password, 10);
});

userSchema.methods.comparePassword = async function(password) {
    return await bcrypt.compare(password, this.password);
};

userSchema.methods.toJSON = function() {
    const user = this.toObject();
    delete user.password;
    return user;
};

module.exports = mongoose.model('User', userSchema);

4.2 models/Post.js #

javascript
const mongoose = require('mongoose');

const postSchema = new mongoose.Schema({
    title: {
        type: String,
        required: [true, '请输入标题'],
        trim: true,
        maxlength: [200, '标题不能超过200个字符']
    },
    content: {
        type: String,
        required: [true, '请输入内容']
    },
    excerpt: {
        type: String,
        maxlength: [300, '摘要不能超过300个字符']
    },
    author: {
        type: mongoose.Schema.Types.ObjectId,
        ref: 'User',
        required: true
    },
    tags: [String],
    status: {
        type: String,
        enum: ['draft', 'published'],
        default: 'draft'
    },
    viewCount: {
        type: Number,
        default: 0
    }
}, {
    timestamps: true
});

postSchema.index({ title: 'text', content: 'text' });

module.exports = mongoose.model('Post', postSchema);

五、路由定义 #

5.1 routes/index.js #

javascript
const express = require('express');
const router = express.Router();

const authRoutes = require('./authRoutes');
const userRoutes = require('./userRoutes');
const postRoutes = require('./postRoutes');

router.use('/auth', authRoutes);
router.use('/users', userRoutes);
router.use('/posts', postRoutes);

router.get('/health', (req, res) => {
    res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

module.exports = router;

5.2 routes/authRoutes.js #

javascript
const express = require('express');
const router = express.Router();
const { body } = require('express-validator');
const authController = require('../controllers/authController');
const { validate } = require('../middlewares/validate');

router.post('/register', [
    body('name').notEmpty().trim(),
    body('email').isEmail().normalizeEmail(),
    body('password').isLength({ min: 6 }),
    validate
], authController.register);

router.post('/login', [
    body('email').isEmail(),
    body('password').notEmpty(),
    validate
], authController.login);

router.post('/refresh-token', authController.refreshToken);

module.exports = router;

5.3 routes/postRoutes.js #

javascript
const express = require('express');
const router = express.Router();
const { body, param, query } = require('express-validator');
const postController = require('../controllers/postController');
const { auth, authorize } = require('../middlewares/auth');
const { validate } = require('../middlewares/validate');

router.get('/', [
    query('page').optional().isInt({ min: 1 }),
    query('limit').optional().isInt({ min: 1, max: 100 }),
    validate
], postController.getAll);

router.get('/:id', [
    param('id').isMongoId(),
    validate
], postController.getById);

router.post('/', auth, [
    body('title').notEmpty().trim(),
    body('content').notEmpty(),
    validate
], postController.create);

router.put('/:id', auth, [
    param('id').isMongoId(),
    body('title').optional().notEmpty().trim(),
    body('content').optional().notEmpty(),
    validate
], postController.update);

router.delete('/:id', auth, [
    param('id').isMongoId(),
    validate
], postController.delete);

module.exports = router;

六、控制器 #

6.1 controllers/postController.js #

javascript
const Post = require('../models/Post');
const asyncHandler = require('../utils/asyncHandler');
const ApiResponse = require('../utils/apiResponse');

exports.getAll = asyncHandler(async (req, res) => {
    const page = parseInt(req.query.page) || 1;
    const limit = parseInt(req.query.limit) || 10;
    const skip = (page - 1) * limit;
    
    const [posts, total] = await Promise.all([
        Post.find({ status: 'published' })
            .populate('author', 'name email')
            .sort({ createdAt: -1 })
            .skip(skip)
            .limit(limit),
        Post.countDocuments({ status: 'published' })
    ]);
    
    ApiResponse.paginated(res, posts, total, page, limit);
});

exports.getById = asyncHandler(async (req, res) => {
    const post = await Post.findByIdAndUpdate(
        req.params.id,
        { $inc: { viewCount: 1 } },
        { new: true }
    ).populate('author', 'name email');
    
    if (!post) {
        return ApiResponse.notFound(res, '文章不存在');
    }
    
    ApiResponse.success(res, post);
});

exports.create = asyncHandler(async (req, res) => {
    const post = await Post.create({
        ...req.body,
        author: req.user.id
    });
    
    ApiResponse.created(res, post, '文章创建成功');
});

exports.update = asyncHandler(async (req, res) => {
    let post = await Post.findById(req.params.id);
    
    if (!post) {
        return ApiResponse.notFound(res, '文章不存在');
    }
    
    if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {
        return ApiResponse.forbidden(res, '无权修改此文章');
    }
    
    post = await Post.findByIdAndUpdate(
        req.params.id,
        { $set: req.body },
        { new: true, runValidators: true }
    );
    
    ApiResponse.success(res, post, '文章更新成功');
});

exports.delete = asyncHandler(async (req, res) => {
    const post = await Post.findById(req.params.id);
    
    if (!post) {
        return ApiResponse.notFound(res, '文章不存在');
    }
    
    if (post.author.toString() !== req.user.id && req.user.role !== 'admin') {
        return ApiResponse.forbidden(res, '无权删除此文章');
    }
    
    await post.deleteOne();
    
    ApiResponse.success(res, null, '文章删除成功');
});

七、工具类 #

7.1 utils/apiResponse.js #

javascript
class ApiResponse {
    static success(res, data, message = '成功') {
        return res.json({
            success: true,
            data,
            message
        });
    }
    
    static created(res, data, message = '创建成功') {
        return res.status(201).json({
            success: true,
            data,
            message
        });
    }
    
    static paginated(res, data, total, page, limit) {
        return res.json({
            success: true,
            data,
            pagination: {
                page,
                limit,
                total,
                totalPages: Math.ceil(total / limit)
            }
        });
    }
    
    static error(res, message, statusCode = 400) {
        return res.status(statusCode).json({
            success: false,
            error: message
        });
    }
    
    static notFound(res, message = '资源未找到') {
        return this.error(res, message, 404);
    }
    
    static unauthorized(res, message = '未授权') {
        return this.error(res, message, 401);
    }
    
    static forbidden(res, message = '禁止访问') {
        return this.error(res, message, 403);
    }
}

module.exports = ApiResponse;

7.2 utils/asyncHandler.js #

javascript
const asyncHandler = fn => (req, res, next) => {
    Promise.resolve(fn(req, res, next)).catch(next);
};

module.exports = asyncHandler;

八、中间件 #

8.1 middlewares/auth.js #

javascript
const jwt = require('jsonwebtoken');
const User = require('../models/User');
const asyncHandler = require('../utils/asyncHandler');

exports.auth = asyncHandler(async (req, res, next) => {
    const authHeader = req.headers['authorization'];
    const token = authHeader?.split(' ')[1];
    
    if (!token) {
        return res.status(401).json({ error: '请先登录' });
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id);
    
    if (!user) {
        return res.status(401).json({ error: '用户不存在' });
    }
    
    req.user = user;
    next();
});

exports.authorize = (...roles) => {
    return (req, res, next) => {
        if (!roles.includes(req.user.role)) {
            return res.status(403).json({ error: '没有权限' });
        }
        next();
    };
};

8.2 middlewares/errorHandler.js #

javascript
const errorHandler = (err, req, res, next) => {
    let statusCode = err.statusCode || 500;
    let message = err.message || '服务器错误';
    
    if (err.name === 'ValidationError') {
        statusCode = 400;
        message = Object.values(err.errors).map(e => e.message).join(', ');
    }
    
    if (err.code === 11000) {
        statusCode = 400;
        message = '数据已存在';
    }
    
    if (err.name === 'JsonWebTokenError') {
        statusCode = 401;
        message = '无效的token';
    }
    
    res.status(statusCode).json({
        success: false,
        error: message,
        stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
    });
};

module.exports = errorHandler;

九、总结 #

RESTful API项目要点:

组件 说明
入口文件 app.js, server.js
路由 定义API端点
控制器 处理请求逻辑
模型 数据结构定义
中间件 认证、验证、错误处理
工具类 响应格式、异步处理

下一步,让我们学习用户认证系统!

最后更新:2026-03-28