Cas d'utilisation
/
Apprentissage et développement
Mettre en œuvre les meilleures pratiques
modèle
•
Agent ninja
Produisez un fichier standard Node.js Express qui respecte les meilleures pratiques RESTful : architecture propre, validation des entrées, journalisation et tests unitaires.
Tableau de bord de l'API RESTful Express Node.js
Voici un schéma standard complet de Express.js qui suit les meilleures pratiques RESTful avec une architecture propre, une validation des entrées, une journalisation et des tests unitaires.
Structure du projet
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.jsMise en œuvre
1. Configuration du package
package.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. Point d'entrée du serveur
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. Configuration de l'application
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. Configuration de l'environnement
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. Gestion des erreurs
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. utilitaire de réponse
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. Intergiciel de validation
src/api/middlewares/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. Modèle utilisateur
src/modèles/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. Référentiel des utilisateurs
src/core/repositories/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. Service aux utilisateurs
src/core/services/user.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. Validation des utilisateurs
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. Contrôleur utilisateur
src/api/controllers/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. Routes des utilisateurs
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. Index des itinéraires 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. Tests unitaires
tests/unité/services/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});tests/unité/contrôleurs/utilisateur.controller.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. Tests d'intégration
tests/intégration/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. Configuration de 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. Variables d'environnement
.env. exemple
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. Configuration d'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 Ignorer
.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.dbExécution de l'application
- Cloner le référentiel
- Copier
.env. exemplepour.envet ajustez selon les besoins - Installez les dépendances :
installation de npm - Exécutez le serveur :
démarrage npmounpm run devpour le développement - Exécutez des tests :
test NPM
Ce modèle standard fournit une base solide pour créer des API RESTful avec Node.js et Express, en suivant les meilleures pratiques en matière d'architecture propre, de validation, de gestion des erreurs et de tests.
Explorez les cas d'utilisation associés
Découvrez l'assistant IA de Ninja
Essayez gratuitement dès aujourd'hui. Forfaits à partir de 19$ par mois.


