Caso de uso
/
Aprendizaje y desarrollo
Implemente las mejores prácticas
modelo
•
Agente ninja
Cree un modelo estándar de Node.js Express que siga las mejores prácticas de RESTful: arquitectura limpia, validación de entradas, registro y pruebas unitarias.
Versión estándar de la API RESTful de Node.js Express
Esta es una plantilla completa de Express.js que sigue las mejores prácticas de RESTful con una arquitectura limpia, validación de entradas, registro y pruebas unitarias.
Estructura del proyecto
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.jsImplementación
1. Configuración del paquete
paquete.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. Punto de entrada al servidor
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. Configuración de la aplicación
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. Configuración del entorno
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. Manejo de errores
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. Utilidad de respuesta
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. Middleware de validación
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. Modelo de usuario
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. Repositorio de usuarios
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. Servicio de usuario
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. Validación de usuario
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. Controlador de usuario
src/api/controladores/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. Rutas de usuario
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. Índice de rutas 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. Pruebas unitarias
pruebas/unidad/servicios/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});pruebas/unidad/controladores/user.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. Pruebas de integración
pruebas/integración/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. Configuración 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 de entorno
Ejemplo de.env.
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. Configuración de 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 Ignore
.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.dbEjecución de la aplicación
- Clonar el repositorio
- Copiar
Ejemplo de.env.a.envy ajústelo según sea necesario - Instale las dependencias:
instalación de npm - Ejecute el servidor:
inicio de npmonpm run devpara el desarrollo - Ejecute pruebas:
prueba npm
Este modelo estándar proporciona una base sólida para crear API RESTful con Node.js y Express, siguiendo las mejores prácticas para una arquitectura limpia, validación, gestión de errores y pruebas.
Explore los casos de uso relacionados
Disfruta del asistente de IA de Ninja
Pruébalo gratis hoy mismo. Planes a partir de 19$ al mes.


