Caso de uso
/
Aprendizagem e desenvolvimento
Implemente as melhores práticas
modelo
•
Agente Ninja
Produza um modelo padrão do Node.js Express que siga as melhores práticas de RESTful: arquitetura limpa, validação de entrada, registro e testes de unidade.
Padrão padrão da API RESTful do Node.js Express
Aqui está um modelo abrangente do Express.js que segue as melhores práticas de RESTful com arquitetura limpa, validação de entrada, registro e testes de unidade.
Estrutura do projeto
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.jsImplantação
1. Configuração do pacote
pacote.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. Ponto de entrada do 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. Configuração do aplicativo
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. Configuração do ambiente
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. Tratamento de erros
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. utilitário de resposta
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 validação
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 usuário
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. Repositório de usuários
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. Atendimento ao usuário
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. Validação do usuário
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 usuário
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. Rotas do usuário
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 rotas de 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. Testes unitários
testes/unidade/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});testes/unidade/controllers/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. Testes de integração
testes/integração/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. Configuração do 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. Variáveis de ambiente
.env.exemplo
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. Configuração 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 Ignorar
.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.dbExecutando o aplicativo
- Clone o repositório
- Copiar
.env.exemplopara.enve ajuste conforme necessário - Instalar dependências:
instalação do npm - Execute o servidor:
início do npmounpm run devpara desenvolvimento - Execute testes:
teste npm
Esse clichê fornece uma base sólida para criar APIs RESTful com Node.js e Express, seguindo as melhores práticas para arquitetura limpa, validação, tratamento de erros e testes.
Explore os casos de uso relacionados
Experimente o assistente de IA do Ninja
Experimente gratuitamente hoje mesmo. Planos a partir de $19/mês.


