Use Case
/
学习与发展
实施最佳实践
Model
•
忍者特工
按照 RESTful 最佳实践(简洁架构、输入验证、日志记录和单元测试)制作 Node.js Express 样板。
Node.js Express RESTful API 样板
这是一个全面的 Express.js 样板,它遵循了 RESTful 最佳实践,包括简洁的架构、输入验证、日志记录和单元测试。
项目结构
1├── src/
2│ ├── api/
3│ │ ├── controllers/
4│ │ │ ├── user.controller.js
5│ │ │ └── index.js
6│ │ ├── middlewares/
7│ │ │ ├── error.middleware.js
8│ │ │ ├── validation.middleware.js
9│ │ │ └── index.js
10│ │ ├── routes/
11│ │ │ ├── user.routes.js
12│ │ │ └── index.js
13│ │ └── validations/
14│ │ ├── user.validation.js
15│ │ └── index.js
16│ ├── config/
17│ │ ├── environment.js
18│ │ └── logger.js
19│ ├── core/
20│ │ ├── repositories/
21│ │ │ └── user.repository.js
22│ │ └── services/
23│ │ └── user.service.js
24│ ├── models/
25│ │ └── user.model.js
26│ ├── utils/
27│ │ ├── api-error.js
28│ │ └── response.js
29│ └── app.js
30├── tests/
31│ ├── integration/
32│ │ └── user.test.js
33│ ├── unit/
34│ │ ├── controllers/
35│ │ │ └── user.controller.test.js
36│ │ └── services/
37│ │ └── user.service.test.js
38│ └── setup.js
39├── .env.example
40├── .eslintrc.js
41├── .gitignore
42├── jest.config.js
43├── package.json
44└── server.js实施
1。包配置
打包.json
1{
2 "name": "express-rest-boilerplate",
3 "version": "1.0.0",
4 "description": "Express RESTful API boilerplate with clean architecture",
5 "main": "server.js",
6 "scripts": {
7 "start": "node server.js",
8 "dev": "nodemon server.js",
9 "lint": "eslint .",
10 "test": "jest",
11 "test:watch": "jest --watch",
12 "test:coverage": "jest --coverage"
13 },
14 "dependencies": {
15 "cors": "^2.8.5",
16 "dotenv": "^16.3.1",
17 "express": "^4.18.2",
18 "express-async-errors": "^3.1.1",
19 "helmet": "^7.0.0",
20 "http-status": "^1.7.0",
21 "joi": "^17.10.1",
22 "morgan": "^1.10.0",
23 "winston": "^3.10.0"
24 },
25 "devDependencies": {
26 "eslint": "^8.49.0",
27 "jest": "^29.6.4",
28 "nodemon": "^3.0.1",
29 "supertest": "^6.3.3"
30 },
31 "engines": {
32 "node": ">=14.0.0"
33 }
34}2。服务器入口点
server.js
1require('dotenv').config();
2const app = require('./src/app');
3const logger = require('./src/config/logger');
4const config = require('./src/config/environment');
5
6const server = app.listen(config.port, () => {
7 logger.info(`Server running on port ${config.port}`);
8});
9
10const exitHandler = () => {
11 if (server) {
12 server.close(() => {
13 logger.info('Server closed');
14 process.exit(1);
15 });
16 } else {
17 process.exit(1);
18 }
19};
20
21const unexpectedErrorHandler = (error) => {
22 logger.error(error);
23 exitHandler();
24};
25
26process.on('uncaughtException', unexpectedErrorHandler);
27process.on('unhandledRejection', unexpectedErrorHandler);
28
29process.on('SIGTERM', () => {
30 logger.info('SIGTERM received');
31 if (server) {
32 server.close();
33 }
34});3.应用程序配置
src/app.js
1require('express-async-errors');
2const express = require('express');
3const helmet = require('helmet');
4const cors = require('cors');
5const morgan = require('morgan');
6const routes = require('./api/routes');
7const { errorHandler } = require('./api/middlewares/error.middleware');
8const logger = require('./config/logger');
9
10const app = express();
11
12// Security middleware
13app.use(helmet());
14
15// Parse JSON request body
16app.use(express.json());
17
18// Parse URL-encoded request body
19app.use(express.urlencoded({ extended: true }));
20
21// Enable CORS
22app.use(cors());
23
24// Request logging
25app.use(morgan('combined', { stream: { write: (message) => logger.http(message.trim()) } }));
26
27// API routes
28app.use('/api', routes);
29
30// Health check endpoint
31app.get('/health', (req, res) => {
32 res.status(200).json({ status: 'ok' });
33});
34
35// Error handling middleware
36app.use(errorHandler);
37
38module.exports = app;4。环境配置
src/config/environment.js
1module.exports = {
2 env: process.env.NODE_ENV || 'development',
3 port: process.env.PORT || 3000,
4 logLevel: process.env.LOG_LEVEL || 'info',
5};src/config/logger.js
1const winston = require('winston');
2const config = require('./environment');
3
4const enumerateErrorFormat = winston.format((info) => {
5 if (info instanceof Error) {
6 Object.assign(info, { message: info.stack });
7 }
8 return info;
9});
10
11const logger = winston.createLogger({
12 level: config.logLevel,
13 format: winston.format.combine(
14 enumerateErrorFormat(),
15 config.env === 'development' ? winston.format.colorize() : winston.format.uncolorize(),
16 winston.format.splat(),
17 winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
18 winston.format.printf(({ timestamp, level, message }) => `${timestamp} ${level}: ${message}`)
19 ),
20 transports: [
21 new winston.transports.Console({
22 stderrLevels: ['error'],
23 }),
24 ],
25});
26
27module.exports = logger;5。错误处理
src/utils/api-error.js
1class ApiError extends Error {
2 constructor(statusCode, message, isOperational = true, stack = '') {
3 super(message);
4 this.statusCode = statusCode;
5 this.isOperational = isOperational;
6 if (stack) {
7 this.stack = stack;
8 } else {
9 Error.captureStackTrace(this, this.constructor);
10 }
11 }
12}
13
14module.exports = ApiError;src/api/middlewares/error.middleware.js
1const httpStatus = require('http-status');
2const logger = require('../../config/logger');
3const ApiError = require('../../utils/api-error');
4const config = require('../../config/environment');
5
6const errorHandler = (err, req, res, next) => {
7 let error = err;
8
9 if (!(error instanceof ApiError)) {
10 const statusCode = error.statusCode || httpStatus.INTERNAL_SERVER_ERROR;
11 const message = error.message || httpStatus[statusCode];
12 error = new ApiError(statusCode, message, false, err.stack);
13 }
14
15 const response = {
16 code: error.statusCode,
17 message: error.message,
18 ...(config.env === 'development' && { stack: error.stack }),
19 };
20
21 if (config.env === 'development') {
22 logger.error(error);
23 }
24
25 res.status(error.statusCode).json(response);
26};
27
28module.exports = {
29 errorHandler,
30};6。响应实用程序
src/utils/response.js
1class ApiResponse {
2 constructor(statusCode, data, message = '') {
3 this.statusCode = statusCode;
4 this.data = data;
5 this.message = message;
6 this.success = statusCode >= 200 && statusCode < 300;
7 }
8
9 static success(data, message = '') {
10 return new ApiResponse(200, data, message);
11 }
12
13 static created(data, message = '') {
14 return new ApiResponse(201, data, message);
15 }
16
17 static noContent(message = '') {
18 return new ApiResponse(204, null, message);
19 }
20}
21
22module.exports = ApiResponse;7。验证中间件
src/api/中间件/validation.middleware.js
1const httpStatus = require('http-status');
2const ApiError = require('../../utils/api-error');
3
4const validate = (schema) => (req, res, next) => {
5 const validSchema = pick(schema, ['params', 'query', 'body']);
6 const object = pick(req, Object.keys(validSchema));
7 const { value, error } = validateSchema(validSchema, object);
8
9 if (error) {
10 const errorMessage = error.details.map((details) => details.message).join(', ');
11 return next(new ApiError(httpStatus.BAD_REQUEST, errorMessage));
12 }
13
14 Object.assign(req, value);
15 return next();
16};
17
18const validateSchema = (schema, data) => {
19 const options = {
20 abortEarly: false, // include all errors
21 allowUnknown: true, // ignore unknown props
22 stripUnknown: true, // remove unknown props
23 };
24
25 const { value, error } = schema.validate(data, options);
26 return { value, error };
27};
28
29const pick = (object, keys) => {
30 return keys.reduce((obj, key) => {
31 if (object && Object.prototype.hasOwnProperty.call(object, key)) {
32 obj[key] = object[key];
33 }
34 return obj;
35 }, {});
36};
37
38module.exports = {
39 validate,
40};8。用户模型
src/models/user.model.js
1// This is a simple in-memory model for demonstration
2// In a real app, you would use a database ORM like Sequelize or Mongoose
3
4class User {
5 constructor(id, name, email) {
6 this.id = id;
7 this.name = name;
8 this.email = email;
9 this.createdAt = new Date();
10 }
11}
12
13module.exports = User;9。用户存储库
src/core/存储库/user.repository.js
1const User = require('../../models/user.model');
2
3// In-memory storage for demonstration
4const users = [];
5let nextId = 1;
6
7class UserRepository {
8 async create(userData) {
9 const user = new User(nextId++, userData.name, userData.email);
10 users.push(user);
11 return user;
12 }
13
14 async findById(id) {
15 return users.find(user => user.id === parseInt(id, 10)) || null;
16 }
17
18 async findAll() {
19 return [...users];
20 }
21
22 async update(id, userData) {
23 const index = users.findIndex(user => user.id === parseInt(id, 10));
24 if (index === -1) return null;
25
26 const updatedUser = { ...users[index], ...userData };
27 users[index] = updatedUser;
28 return updatedUser;
29 }
30
31 async delete(id) {
32 const index = users.findIndex(user => user.id === parseInt(id, 10));
33 if (index === -1) return false;
34
35 users.splice(index, 1);
36 return true;
37 }
38}
39
40module.exports = new UserRepository();10。用户服务
src/core/service/service.js
1const httpStatus = require('http-status');
2const userRepository = require('../repositories/user.repository');
3const ApiError = require('../../utils/api-error');
4
5class UserService {
6 async createUser(userData) {
7 return userRepository.create(userData);
8 }
9
10 async getUserById(id) {
11 const user = await userRepository.findById(id);
12 if (!user) {
13 throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
14 }
15 return user;
16 }
17
18 async getUsers() {
19 return userRepository.findAll();
20 }
21
22 async updateUser(id, userData) {
23 const user = await userRepository.update(id, userData);
24 if (!user) {
25 throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
26 }
27 return user;
28 }
29
30 async deleteUser(id) {
31 const deleted = await userRepository.delete(id);
32 if (!deleted) {
33 throw new ApiError(httpStatus.NOT_FOUND, 'User not found');
34 }
35 return { success: true };
36 }
37}
38
39module.exports = new UserService();11。用户验证
src/api/validations/user.validation.js
1const Joi = require('joi');
2
3const createUser = {
4 body: Joi.object().keys({
5 name: Joi.string().required(),
6 email: Joi.string().email().required(),
7 }),
8};
9
10const getUser = {
11 params: Joi.object().keys({
12 id: Joi.number().integer().required(),
13 }),
14};
15
16const updateUser = {
17 params: Joi.object().keys({
18 id: Joi.number().integer().required(),
19 }),
20 body: Joi.object().keys({
21 name: Joi.string(),
22 email: Joi.string().email(),
23 }).min(1),
24};
25
26const deleteUser = {
27 params: Joi.object().keys({
28 id: Joi.number().integer().required(),
29 }),
30};
31
32module.exports = {
33 createUser,
34 getUser,
35 updateUser,
36 deleteUser,
37};12。用户控制器
src/api/控制器/user.controller.js
1const httpStatus = require('http-status');
2const userService = require('../../core/services/user.service');
3const ApiResponse = require('../../utils/response');
4
5const createUser = async (req, res) => {
6 const user = await userService.createUser(req.body);
7 res.status(httpStatus.CREATED).json(ApiResponse.created(user, 'User created successfully'));
8};
9
10const getUser = async (req, res) => {
11 const user = await userService.getUserById(req.params.id);
12 res.status(httpStatus.OK).json(ApiResponse.success(user));
13};
14
15const getUsers = async (req, res) => {
16 const users = await userService.getUsers();
17 res.status(httpStatus.OK).json(ApiResponse.success(users));
18};
19
20const updateUser = async (req, res) => {
21 const user = await userService.updateUser(req.params.id, req.body);
22 res.status(httpStatus.OK).json(ApiResponse.success(user, 'User updated successfully'));
23};
24
25const deleteUser = async (req, res) => {
26 await userService.deleteUser(req.params.id);
27 res.status(httpStatus.NO_CONTENT).send();
28};
29
30module.exports = {
31 createUser,
32 getUser,
33 getUsers,
34 updateUser,
35 deleteUser,
36};13。用户路线
src/api/routes/user.routes.js
1const express = require('express');
2const { validate } = require('../middlewares/validation.middleware');
3const userValidation = require('../validations/user.validation');
4const userController = require('../controllers/user.controller');
5
6const router = express.Router();
7
8router
9 .route('/')
10 .post(validate(userValidation.createUser), userController.createUser)
11 .get(userController.getUsers);
12
13router
14 .route('/:id')
15 .get(validate(userValidation.getUser), userController.getUser)
16 .patch(validate(userValidation.updateUser), userController.updateUser)
17 .delete(validate(userValidation.deleteUser), userController.deleteUser);
18
19module.exports = router;14。API 路由索引
src/api/routes/index.js
1const express = require('express');
2const userRoutes = require('./user.routes');
3
4const router = express.Router();
5
6const routes = [
7 {
8 path: '/users',
9 route: userRoutes,
10 },
11];
12
13routes.forEach((route) => {
14 router.use(route.path, route.route);
15});
16
17module.exports = router;15。单元测试
测试/单元/服务/user.service.test.js
1const httpStatus = require('http-status');
2const userService = require('../../../src/core/services/user.service');
3const userRepository = require('../../../src/core/repositories/user.repository');
4const ApiError = require('../../../src/utils/api-error');
5
6// Mock the repository
7jest.mock('../../../src/core/repositories/user.repository');
8
9describe('User service', () => {
10 describe('createUser', () => {
11 test('should create a user successfully', async () => {
12 const userData = { name: 'Test User', email: 'test@example.com' };
13 const expectedUser = { id: 1, ...userData, createdAt: expect.any(Date) };
14
15 userRepository.create.mockResolvedValue(expectedUser);
16
17 const result = await userService.createUser(userData);
18
19 expect(userRepository.create).toHaveBeenCalledWith(userData);
20 expect(result).toEqual(expectedUser);
21 });
22 });
23
24 describe('getUserById', () => {
25 test('should return user if found', async () => {
26 const userId = 1;
27 const expectedUser = { id: userId, name: 'Test User', email: 'test@example.com' };
28
29 userRepository.findById.mockResolvedValue(expectedUser);
30
31 const result = await userService.getUserById(userId);
32
33 expect(userRepository.findById).toHaveBeenCalledWith(userId);
34 expect(result).toEqual(expectedUser);
35 });
36
37 test('should throw error if user not found', async () => {
38 const userId = 999;
39
40 userRepository.findById.mockResolvedValue(null);
41
42 await expect(userService.getUserById(userId)).rejects.toThrow(
43 new ApiError(httpStatus.NOT_FOUND, 'User not found')
44 );
45
46 expect(userRepository.findById).toHaveBeenCalledWith(userId);
47 });
48 });
49
50 // Additional tests for other methods would follow the same pattern
51});测试/单元/控制器/用户控制器.test.js
1const httpStatus = require('http-status');
2const userController = require('../../../src/api/controllers/user.controller');
3const userService = require('../../../src/core/services/user.service');
4const ApiResponse = require('../../../src/utils/response');
5
6// Mock the service
7jest.mock('../../../src/core/services/user.service');
8
9describe('User controller', () => {
10 let mockReq;
11 let mockRes;
12 let mockNext;
13
14 beforeEach(() => {
15 mockReq = {};
16 mockRes = {
17 status: jest.fn().mockReturnThis(),
18 json: jest.fn(),
19 send: jest.fn(),
20 };
21 mockNext = jest.fn();
22 });
23
24 describe('createUser', () => {
25 test('should create a user and return 201 status', async () => {
26 const userData = { name: 'Test User', email: 'test@example.com' };
27 const createdUser = { id: 1, ...userData };
28 mockReq.body = userData;
29
30 userService.createUser.mockResolvedValue(createdUser);
31
32 await userController.createUser(mockReq, mockRes);
33
34 expect(userService.createUser).toHaveBeenCalledWith(userData);
35 expect(mockRes.status).toHaveBeenCalledWith(httpStatus.CREATED);
36 expect(mockRes.json).toHaveBeenCalledWith(
37 ApiResponse.created(createdUser, 'User created successfully')
38 );
39 });
40 });
41
42 describe('getUser', () => {
43 test('should get a user by id and return 200 status', async () => {
44 const userId = '1';
45 const user = { id: 1, name: 'Test User', email: 'test@example.com' };
46 mockReq.params = { id: userId };
47
48 userService.getUserById.mockResolvedValue(user);
49
50 await userController.getUser(mockReq, mockRes);
51
52 expect(userService.getUserById).toHaveBeenCalledWith(userId);
53 expect(mockRes.status).toHaveBeenCalledWith(httpStatus.OK);
54 expect(mockRes.json).toHaveBeenCalledWith(ApiResponse.success(user));
55 });
56 });
57
58 // Additional tests for other controller methods would follow the same pattern
59});16。集成测试
测试/集成/user.test.js
1const request = require('supertest');
2const httpStatus = require('http-status');
3const app = require('../../src/app');
4
5describe('User API', () => {
6 let newUser;
7 let userId;
8
9 beforeAll(async () => {
10 // Clear any existing data or set up test database
11 });
12
13 describe('POST /api/users', () => {
14 test('should create a new user', async () => {
15 newUser = {
16 name: 'Integration Test User',
17 email: 'integration@test.com',
18 };
19
20 const res = await request(app)
21 .post('/api/users')
22 .send(newUser)
23 .expect(httpStatus.CREATED);
24
25 expect(res.body.data).toHaveProperty('id');
26 expect(res.body.data.name).toBe(newUser.name);
27 expect(res.body.data.email).toBe(newUser.email);
28
29 userId = res.body.data.id;
30 });
31
32 test('should return 400 error if email is invalid', async () => {
33 const invalidUser = {
34 name: 'Invalid User',
35 email: 'not-an-email',
36 };
37
38 await request(app)
39 .post('/api/users')
40 .send(invalidUser)
41 .expect(httpStatus.BAD_REQUEST);
42 });
43 });
44
45 describe('GET /api/users/:id', () => {
46 test('should get user by id', async () => {
47 const res = await request(app)
48 .get(`/api/users/${userId}`)
49 .expect(httpStatus.OK);
50
51 expect(res.body.data).toHaveProperty('id', userId);
52 expect(res.body.data.name).toBe(newUser.name);
53 expect(res.body.data.email).toBe(newUser.email);
54 });
55
56 test('should return 404 error if user does not exist', async () => {
57 await request(app)
58 .get('/api/users/999')
59 .expect(httpStatus.NOT_FOUND);
60 });
61 });
62
63 // Additional integration tests for other endpoints would follow the same pattern
64});17。Jest 配置
jest.config.js
1module.exports = {
2 testEnvironment: 'node',
3 testMatch: ['**/tests/**/*.test.js'],
4 collectCoverageFrom: [
5 'src/**/*.js',
6 '!src/app.js',
7 '!src/config/**/*.js',
8 ],
9 coverageDirectory: 'coverage',
10 setupFilesAfterEnv: ['./tests/setup.js'],
11};tests/setup.js
1// Setup file for Jest
2// You can add global test setup code here18。环境变量
.env.example
1# Server Configuration
2NODE_ENV=development
3PORT=3000
4LOG_LEVEL=info
5
6# Add other environment variables as needed
7# e.g., DATABASE_URL, JWT_SECRET, etc.19。ESLint 配置
.eslintrc.js
1module.exports = {
2 env: {
3 node: true,
4 jest: true,
5 },
6 extends: ['eslint:recommended'],
7 parserOptions: {
8 ecmaVersion: 2020,
9 },
10 rules: {
11 'no-console': 'warn',
12 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
13 },
14};20。Git 忽略
.gitignore
1# Dependencies
2node_modules/
3
4# Logs
5logs
6*.log
7npm-debug.log*
8
9# Environment variables
10.env
11.env.local
12.env.development
13.env.test
14.env.production
15
16# Coverage directory
17coverage/
18
19# IDE
20.idea/
21.vscode/
22*.sublime-project
23*.sublime-workspace
24
25# OS
26.DS_Store
27Thumbs.db运行应用程序
- 克隆存储库
- 复制
.env.example到.env并根据需要进行调整 - 安装依赖项:
npm 安装 - 运行服务器:
npm 启动要么npm 运行开发用于开发 - 运行测试:
npm 测试
该样板为使用 Node.js 和 Express 构建 RESTful API 提供了坚实的基础,遵循了整洁架构、验证、错误处理和测试的最佳实践。
Experience Ninja's AI Assistant
Try for free today. Plans starting at $19/month.


