Código Limpo - Konecty Backend
Princípios Fundamentais
KISS (Keep It Simple, Stupid)
Mantenha simples. Simplicidade é a maior sofisticação.
- Funções com uma responsabilidade clara
- Lógica direta e fácil de entender
- Evite abstrações prematuras
- Se parece complexo, provavelmente está errado
YAGNI (You Aren't Gonna Need It)
Não implemente o que não precisa agora.
- Resolva o problema atual, não problemas futuros hipotéticos
- Adicionar complexidade só quando comprovadamente necessária
- É mais fácil adicionar depois do que remover agora
DRY (Don't Repeat Yourself)
Não se repita.
- Extraia lógica duplicada em funções/módulos
- Use middlewares para lógica compartilhada
- Constantes em arquivos de configuração
SOLID
Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion
- S: Um módulo/função = uma responsabilidade
- O: Aberto para extensão, fechado para modificação
- L: Subtipos devem ser substituíveis por seus tipos base
- I: Interfaces pequenas e específicas
- D: Dependa de abstrações, não de implementações concretas
Regras de Código
1. prefer-const
Sempre use const, nunca let (exceto quando mutação é necessária).
typescript
1// ❌ Errado
2let users = [];
3for (let i = 0; i < results.length; i++) {
4 users.push(results[i]);
5}
6
7// ✅ Correto
8const users = results.map(result => result);
Por quê?
- Previne mutações acidentais
- Código mais previsível
- Facilita debugging
2. Programação Funcional
Use map, reduce, filter, flatMap ao invés de for loops.
typescript
1// ❌ Errado
2let activeUsers = [];
3for (let i = 0; i < users.length; i++) {
4 if (users[i].active) {
5 activeUsers.push(users[i].id);
6 }
7}
8
9// ✅ Correto
10const activeUsers = users
11 .filter(user => user.active)
12 .map(user => user.id);
Por quê?
- Mais expressivo e declarativo
- Menos propenso a erros
- Melhor para composição
3. Evitar while(true)
Nunca use while(true). Prefira recursão ou condições explícitas.
typescript
1// ❌ Errado
2while (true) {
3 const job = await queue.getNext();
4 if (!job) break;
5 await processJob(job);
6}
7
8// ✅ Correto (Recursivo)
9const processQueue = async (): Promise<void> => {
10 const job = await queue.getNext();
11 if (!job) return;
12 await processJob(job);
13 return processQueue();
14};
15
16// ✅ Correto (Iterativo com condição explícita)
17let hasMoreJobs = true;
18while (hasMoreJobs) {
19 const job = await queue.getNext();
20 hasMoreJobs = job !== null;
21 if (hasMoreJobs) {
22 await processJob(job);
23 }
24}
4. no-magic-numbers
Todos os números devem ser constantes nomeadas.
typescript
1// ❌ Errado
2if (users.length > 100) {
3 await sendEmail(admin, 'Too many users');
4}
5setTimeout(retry, 5000);
6
7// ✅ Correto
8const MAX_USERS_BEFORE_ALERT = 100;
9const RETRY_DELAY_MS = 5000;
10
11if (users.length > MAX_USERS_BEFORE_ALERT) {
12 await sendEmail(admin, 'Too many users');
13}
14setTimeout(retry, RETRY_DELAY_MS);
Constantes Comuns:
typescript
1// Tempo
2const MILLISECONDS_PER_SECOND = 1000;
3const SECONDS_PER_MINUTE = 60;
4const MINUTES_PER_HOUR = 60;
5const HOURS_PER_DAY = 24;
6
7// Database
8const DEFAULT_PAGE_SIZE = 20;
9const MAX_PAGE_SIZE = 100;
10const CONNECTION_POOL_SIZE = 10;
11
12// API
13const DEFAULT_TIMEOUT_MS = 30_000;
14const MAX_RETRIES = 3;
15const RATE_LIMIT_PER_MINUTE = 60;
5. Controle de Concorrência Assíncrona
⚠️ Nota: Bluebird está deprecated em 2026. Use alternativas modernas abaixo.
Problema: Executar múltiplas operações assíncronas com controle de paralelismo.
Opção 1: p-limit (Recomendada)
typescript
1import pLimit from 'p-limit';
2
3const API_CONCURRENCY = 5;
4
5const sendEmails = async (users: User[]): Promise<void> => {
6 const limit = pLimit(API_CONCURRENCY);
7
8 await Promise.all(
9 users.map(user => limit(() => sendEmail(user)))
10 );
11};
Vantagens: Simples, mantido, funcional puro.
Opção 2: Chunking Funcional (Sem Dependências)
typescript
1const chunk = <T>(array: T[], size: number): T[][] =>
2 Array.from(
3 { length: Math.ceil(array.length / size) },
4 (_, index) => array.slice(index * size, (index + 1) * size)
5 );
6
7const DB_BATCH_SIZE = 10;
8
9const processDocuments = async (docs: Document[]): Promise<void> => {
10 const chunks = chunk(docs, DB_BATCH_SIZE);
11
12 await chunks.reduce(
13 async (previousBatch, currentChunk) => {
14 await previousBatch;
15 return Promise.all(currentChunk.map(processDoc));
16 },
17 Promise.resolve([])
18 );
19};
Vantagens: Sem dependências, funcional.
Opção 3: Bluebird (Legado - Deprecated)
typescript
1// ⚠️ NÃO USE EM CÓDIGO NOVO - Apenas para manutenção de código legado
2import Bluebird from 'bluebird';
3
4const CONCURRENCY_LIMIT = 5;
5await Bluebird.map(users, sendEmail, { concurrency: CONCURRENCY_LIMIT });
Limites de Concorrência Recomendados
typescript
1// Operações de Banco de Dados
2const DB_CONCURRENCY = 10;
3
4// Chamadas de API Externa
5const API_CONCURRENCY = 5;
6
7// Operações de I/O (arquivos)
8const IO_CONCURRENCY = 3;
9
10// Processamento Pesado (CPU-bound)
11const CPU_CONCURRENCY = 2;
Comparação
| Opção | Prós | Contras | Quando Usar |
|---|
| p-limit | Simples, mantido | Dependência externa | Padrão para novo código |
| Chunking | Sem deps, funcional | Mais verbose | Projetos sem dependências extras |
| Bluebird | - | Deprecated | Apenas código legado |
Quando aplicar controle de concorrência:
- ✅ Processar múltiplos documentos do DB
- ✅ Fazer múltiplas chamadas HTTP
- ✅ Processar múltiplos arquivos
- ❌ Operações muito rápidas (< 10ms)
- ❌ Já existe controle no destino (pool do DB)
Padrões Node.js/TypeScript Backend
Async/Await
Sempre prefira async/await sobre callbacks.
typescript
1// ❌ Evite callbacks
2db.find({ active: true }, (err, users) => {
3 if (err) return handleError(err);
4 processUsers(users);
5});
6
7// ✅ Use async/await
8const findActiveUsers = async (): Promise<User[]> => {
9 try {
10 const users = await db.find({ active: true });
11 return users;
12 } catch (error) {
13 handleError(error);
14 throw error;
15 }
16};
Interfaces e Types
Sempre defina tipos para parâmetros e retornos.
typescript
1// ❌ Sem tipagem
2const processUser = async (id, options) => { ... }
3
4// ✅ Com tipagem completa
5interface ProcessUserOptions {
6 sendEmail?: boolean;
7 updateCache?: boolean;
8}
9
10const processUser = async (
11 id: string,
12 options: ProcessUserOptions = {}
13): Promise<User> => { ... }
Error Handling Centralizado
Use express-async-errors + hierarquia de errors + middleware centralizado.
1. Instalar Dependência
bash
1npm install express-async-errors
2. Criar Hierarquia de Errors
typescript
1// types/errors.ts
2export class AppError extends Error {
3 constructor(
4 message: string,
5 public statusCode: number = 500,
6 public details?: string[]
7 ) {
8 super(message);
9 this.name = this.constructor.name;
10 Error.captureStackTrace(this, this.constructor);
11 }
12}
13
14export class ValidationError extends AppError {
15 constructor(message: string, details: string[] = []) {
16 super(message, 400, details);
17 }
18}
19
20export class NotFoundError extends AppError {
21 constructor(message = 'Resource not found') {
22 super(message, 404);
23 }
24}
25
26export class UnauthorizedError extends AppError {
27 constructor(message = 'Unauthorized') {
28 super(message, 401);
29 }
30}
31
32export class ForbiddenError extends AppError {
33 constructor(message = 'Forbidden') {
34 super(message, 403);
35 }
36}
37
38export class ConflictError extends AppError {
39 constructor(message = 'Conflict') {
40 super(message, 409);
41 }
42}
3. Middleware de Error Handling
typescript
1// middleware/errorHandler.ts
2import { Request, Response, NextFunction } from 'express';
3import { AppError } from '../types/errors';
4import { logger } from '../lib/logger';
5
6const HTTP_INTERNAL_ERROR = 500;
7const HTTP_BAD_REQUEST = 400;
8
9export const errorHandler = (
10 err: Error | AppError,
11 req: Request,
12 res: Response,
13 next: NextFunction
14): void => {
15 let statusCode = HTTP_INTERNAL_ERROR;
16 let message = 'Internal Server Error';
17 let details: string[] | undefined;
18
19 // AppError e subclasses
20 if (err instanceof AppError) {
21 statusCode = err.statusCode;
22 message = err.message;
23 details = err.details;
24 }
25 // Mongoose ValidationError
26 else if (err.name === 'ValidationError') {
27 statusCode = HTTP_BAD_REQUEST;
28 message = 'Validation failed';
29 details = Object.values((err as any).errors).map((e: any) => e.message);
30 }
31 // Mongoose CastError
32 else if (err.name === 'CastError') {
33 statusCode = HTTP_BAD_REQUEST;
34 message = 'Invalid ID format';
35 }
36 // MongoDB Duplicate Key
37 else if ((err as any).code === 11000) {
38 statusCode = 409;
39 message = 'Duplicate entry';
40 const field = Object.keys((err as any).keyPattern)[0];
41 details = [`${field} already exists`];
42 }
43
44 // Log estruturado
45 logger.error({
46 error: {
47 name: err.name,
48 message: err.message,
49 stack: err.stack,
50 },
51 statusCode,
52 path: req.path,
53 method: req.method,
54 body: req.body,
55 userId: (req as any).user?.id,
56 }, 'Request error');
57
58 // Resposta padronizada
59 res.status(statusCode).json({
60 success: false,
61 message,
62 ...(details && { details }),
63 timestamp: new Date().toISOString(),
64 path: req.path,
65 });
66};
4. Setup no App
typescript
1// app.ts
2import 'express-async-errors'; // ⚠️ IMPORTANTE: importar NO TOPO
3import express from 'express';
4import { errorHandler } from './middleware/errorHandler';
5
6const app = express();
7
8app.use(express.json());
9
10// ... suas rotas aqui ...
11
12// ⚠️ CRÍTICO: errorHandler deve ser o ÚLTIMO middleware
13app.use(errorHandler);
14
15export default app;
5. Uso nas Rotas
typescript
1// routes/users.ts
2import { NotFoundError, ValidationError } from '../types/errors';
3
4const HTTP_CREATED = 201;
5
6// Com express-async-errors, não precisa try-catch manual
7export const createUser = async (req: Request, res: Response): Promise<void> => {
8 // Validação com Zod
9 const validatedInput = CreateUserSchema.parse(req.body); // Lança erro se inválido
10
11 // Verificações de negócio
12 const existingUser = await User.findOne({ email: validatedInput.email });
13 if (existingUser) {
14 throw new ConflictError('Email already in use');
15 }
16
17 const user = await User.create(validatedInput);
18
19 res.status(HTTP_CREATED).json({
20 success: true,
21 data: {
22 id: user._id.toString(),
23 name: user.name,
24 email: user.email,
25 },
26 });
27};
28
29export const getUserById = async (req: Request, res: Response): Promise<void> => {
30 const user = await User.findById(req.params.id);
31
32 if (!user) {
33 throw new NotFoundError(`User ${req.params.id} not found`);
34 }
35
36 res.json({
37 success: true,
38 data: user,
39 });
40};
Vantagens do Error Handling Centralizado:
- Consistência em todas as respostas de erro
- Menos código duplicado
- Logging automático de todos os errors
- Fácil adicionar novos tipos de erro
- Async errors capturados automaticamente
Padrões Async Modernos
for-await-of para Streams e Iterables
Use para processar streams assíncronos.
typescript
1// Processar stream de dados
2const processStream = async (stream: AsyncIterable<Chunk>): Promise<void> => {
3 const results: Result[] = [];
4
5 for await (const chunk of stream) {
6 const processed = await processChunk(chunk);
7 results.push(processed);
8 }
9
10 return results;
11};
12
13// Async generator
14async function* fetchPaginated(endpoint: string): AsyncGenerator<User[]> {
15 const PAGE_SIZE = 100;
16 let page = 1;
17 let hasMore = true;
18
19 while (hasMore) {
20 const response = await api.get(`${endpoint}?page=${page}&size=${PAGE_SIZE}`);
21 yield response.data;
22 hasMore = response.data.length === PAGE_SIZE;
23 page++;
24 }
25}
26
27// Uso
28const allUsers: User[] = [];
29for await (const userBatch of fetchPaginated('/users')) {
30 allUsers.push(...userBatch);
31}
Promise.allSettled para Operações Independentes
Use quando algumas operações podem falhar sem afetar outras.
typescript
1// ✅ Promise.allSettled - continua mesmo com falhas
2const sendNotifications = async (users: User[]): Promise<void> => {
3 const results = await Promise.allSettled(
4 users.map(user => sendEmail(user))
5 );
6
7 const succeeded = results.filter(r => r.status === 'fulfilled').length;
8 const failed = results.filter(r => r.status === 'rejected').length;
9
10 logger.info({ succeeded, failed }, 'Notifications sent');
11
12 // Log apenas os que falharam
13 results.forEach((result, index) => {
14 if (result.status === 'rejected') {
15 logger.error({
16 userId: users[index].id,
17 error: result.reason
18 }, 'Failed to send email');
19 }
20 });
21};
22
23// ❌ Promise.all - falha tudo se um falhar
24const sendNotificationsBad = async (users: User[]): Promise<void> => {
25 await Promise.all(users.map(sendEmail)); // Um erro para tudo
26};
Quando usar cada um:
| Método | Quando Usar | Comportamento |
|---|
Promise.all | Todas devem suceder | Rejeita no primeiro erro |
Promise.allSettled | Independentes, algumas podem falhar | Sempre resolve, retorna status de cada |
Promise.race | Primeiro a completar vence | Resolve/rejeita com primeiro resultado |
Promise.any | Qualquer sucesso serve | Resolve com primeiro sucesso |
Logging Estruturado
Use Pino para logging estruturado.
typescript
1import pino from 'pino';
2
3const logger = pino({
4 level: process.env.LOG_LEVEL || 'info',
5});
6
7// ✅ Log estruturado
8logger.info({ userId, action: 'login' }, 'User logged in');
9logger.error({ error, userId }, 'Failed to process user');
10
11// ❌ Evite console.log
12console.log('User logged in:', userId);
Estrutura de Arquivos
Organização por Feature
src/
├── imports/
│ ├── auth/
│ │ ├── login.ts
│ │ ├── otp.ts
│ │ └── types.ts
│ ├── data/
│ │ └── api/
│ │ ├── find.ts
│ │ ├── create.ts
│ │ └── update.ts
│ └── utils/
│ ├── dateUtils.ts
│ └── validators.ts
└── server/
├── routes/
│ └── api/
└── middleware/
Nomenclatura
- Módulos/Arquivos: camelCase (
userService.ts)
- Classes: PascalCase (
UserService)
- Funções: camelCase (
findUserById)
- Constants: UPPER_SNAKE_CASE
- Types/Interfaces: PascalCase (
UserDocument, ApiResponse)
Validação e Type Safety
Zod para Validação
Use Zod para validar dados de entrada.
typescript
1import { z } from 'zod';
2
3const CreateUserSchema = z.object({
4 name: z.string().min(1),
5 email: z.string().email(),
6 age: z.number().positive().optional(),
7});
8
9type CreateUserInput = z.infer<typeof CreateUserSchema>;
10
11export const createUser = async (input: unknown): Promise<User> => {
12 const validatedInput = CreateUserSchema.parse(input);
13 // validatedInput é tipado como CreateUserInput
14 return db.users.create(validatedInput);
15};
TypeScript Strict Mode
Sempre use strict mode.
json
1// tsconfig.json
2{
3 "compilerOptions": {
4 "strict": true,
5 "noImplicitAny": true,
6 "strictNullChecks": true,
7 "strictFunctionTypes": true,
8 "esModuleInterop": true
9 }
10}
Database (MongoDB)
Defina schemas e types corretamente.
typescript
1import { Schema, model, Document } from 'mongoose';
2
3interface IUser extends Document {
4 name: string;
5 email: string;
6 active: boolean;
7 createdAt: Date;
8}
9
10const UserSchema = new Schema<IUser>({
11 name: { type: String, required: true },
12 email: { type: String, required: true, unique: true },
13 active: { type: Boolean, default: true },
14 createdAt: { type: Date, default: Date.now },
15});
16
17export const User = model<IUser>('User', UserSchema);
Queries Eficientes
typescript
1// ✅ Use lean() para leitura quando não precisa de document
2const users = await User.find({ active: true }).lean();
3
4// ✅ Use select para buscar apenas campos necessários
5const userEmails = await User.find({ active: true })
6 .select('email')
7 .lean();
8
9// ✅ Use indexes apropriados
10UserSchema.index({ email: 1 });
11UserSchema.index({ active: 1, createdAt: -1 });
API Design
Middleware Pattern
Use middlewares para lógica compartilhada.
typescript
1// Middleware de autenticação
2export const authenticate = async (req: Request, res: Response, next: NextFunction) => {
3 try {
4 const token = req.headers.authorization?.replace('Bearer ', '');
5 if (!token) {
6 throw new UnauthorizedError('No token provided');
7 }
8
9 const decoded = await verifyToken(token);
10 req.user = decoded;
11 next();
12 } catch (error) {
13 next(error);
14 }
15};
16
17// Uso
18router.get('/protected', authenticate, async (req, res) => {
19 res.json({ user: req.user });
20});
Error Handling Middleware
Centralize tratamento de erros.
typescript
1export const errorHandler = (
2 error: Error,
3 req: Request,
4 res: Response,
5 next: NextFunction
6) => {
7 logger.error({ error, path: req.path }, 'Request error');
8
9 if (error instanceof AppError) {
10 return res.status(error.statusCode).json({
11 error: error.message,
12 code: error.code,
13 });
14 }
15
16 res.status(500).json({
17 error: 'Internal server error',
18 });
19};
Response Patterns
Padronize respostas da API.
typescript
1// Success
2interface SuccessResponse<T> {
3 success: true;
4 data: T;
5}
6
7// Error
8interface ErrorResponse {
9 success: false;
10 error: string;
11 code?: string;
12}
13
14// Helper
15const sendSuccess = <T>(res: Response, data: T) => {
16 res.json({ success: true, data });
17};
18
19const sendError = (res: Response, statusCode: number, message: string) => {
20 res.status(statusCode).json({ success: false, error: message });
21};
Connection Pooling
Use connection pooling para database.
typescript
1const MONGODB_OPTIONS = {
2 maxPoolSize: 10,
3 minPoolSize: 2,
4 socketTimeoutMS: 45000,
5};
6
7await mongoose.connect(MONGODB_URI, MONGODB_OPTIONS);
Cache operações custosas.
typescript
1import Redis from 'ioredis';
2
3const redis = new Redis({
4 host: process.env.REDIS_HOST,
5 port: Number(process.env.REDIS_PORT),
6});
7
8const CACHE_TTL_SECONDS = 300; // 5 minutes
9
10const getCachedUser = async (id: string): Promise<User | null> => {
11 const cached = await redis.get(`user:${id}`);
12 if (cached) {
13 return JSON.parse(cached);
14 }
15
16 const user = await User.findById(id).lean();
17 if (user) {
18 await redis.setex(`user:${id}`, CACHE_TTL_SECONDS, JSON.stringify(user));
19 }
20
21 return user;
22};
Graceful Shutdown
Implemente shutdown gracioso.
typescript
1const gracefulShutdown = async () => {
2 logger.info('Shutting down gracefully...');
3
4 // Pare de aceitar novas conexões
5 server.close();
6
7 // Aguarde requisições em andamento
8 await Promise.all([
9 mongoose.connection.close(),
10 redis.quit(),
11 ]);
12
13 logger.info('Shutdown complete');
14 process.exit(0);
15};
16
17process.on('SIGTERM', gracefulShutdown);
18process.on('SIGINT', gracefulShutdown);
Segurança
Sempre valide entrada do usuário.
typescript
1// Use Zod para validação
2const input = InputSchema.parse(req.body);
Environment Variables
Use variáveis de ambiente para configuração sensível.
typescript
1// .env (não commitar)
2DATABASE_URL=mongodb://...
3JWT_SECRET=...
4API_KEY=...
5
6// Uso
7import dotenv from 'dotenv';
8dotenv.config();
9
10const config = {
11 databaseUrl: process.env.DATABASE_URL!,
12 jwtSecret: process.env.JWT_SECRET!,
13};
Rate Limiting
Implemente rate limiting.
typescript
1import rateLimit from 'express-rate-limit';
2
3const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes
4const MAX_REQUESTS_PER_WINDOW = 100;
5
6const limiter = rateLimit({
7 windowMs: RATE_LIMIT_WINDOW_MS,
8 max: MAX_REQUESTS_PER_WINDOW,
9 message: 'Too many requests from this IP',
10});
11
12app.use('/api/', limiter);
Testes
Unit Tests
Teste lógica de negócio isoladamente.
typescript
1import { findActiveUsers } from './userService';
2
3describe('userService', () => {
4 describe('findActiveUsers', () => {
5 it('should return only active users', async () => {
6 const users = await findActiveUsers();
7
8 expect(users).toHaveLength(2);
9 expect(users.every(u => u.active)).toBe(true);
10 });
11 });
12});
Integration Tests
Teste fluxos completos.
typescript
1describe('POST /api/users', () => {
2 it('should create user and return 201', async () => {
3 const response = await request(app)
4 .post('/api/users')
5 .send({
6 name: 'Test User',
7 email: 'test@example.com',
8 });
9
10 expect(response.status).toBe(201);
11 expect(response.body.data).toHaveProperty('id');
12 });
13});
Checklist Rápido
Antes de commitar código, verifique:
Recursos Adicionais
Lembre-se: Código limpo não é sobre seguir regras cegamente, mas sobre escrever código que é fácil de entender, manter e modificar. Use bom senso!