Use Case

/

学习与发展

实施最佳实践

Model

忍者特工

按照 RESTful 最佳实践(简洁架构、输入验证、日志记录和单元测试)制作 Node.js Express 样板。

Try Query
忍者特工
/
编码员

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 here

18。环境变量

.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

运行应用程序

  1. 克隆存储库
  2. 复制 .env.example.env 并根据需要进行调整
  3. 安装依赖项: npm 安装
  4. 运行服务器: npm 启动 要么 npm 运行开发 用于开发
  5. 运行测试: npm 测试

该样板为使用 Node.js 和 Express 构建 RESTful API 提供了坚实的基础,遵循了整洁架构、验证、错误处理和测试的最佳实践。

Experience Ninja's AI Assistant

Try for free today. Plans starting at $19/month.