Best Practices: El Manual de Supervivencia del Desarrollador Profesional
En 10+ años construyendo sistemas críticos, he aprendido que las best practices no son sugerencias, son la diferencia entre dormir tranquilo o recibir llamadas a las 3 AM.
Este no es un artículo teórico. Son prácticas que uso todos los días en MATIAS IMPULSO y que salvaron millones en proyectos bancarios.
Por Qué las Best Practices No Son Opcionales
Historia Real: El Deploy del Viernes que Costó $200K
Escenario: Fintech con 100K usuarios activos, deploy manual sin tests.
Viernes 4:45 PM:
git push origin main
# "Se ve bien en mi máquina" 🔥
Viernes 5:30 PM:
- 🔥 API caída
- 💸 Sistema de pagos offline
- 📞 1,200 tickets de soporte
- 😰 3 developers llamados de emergencia
Lunes 9:00 AM:
- 💰 $200K en compensaciones
- 📰 Nota de prensa negativa
- 👥 2 clientes enterprise cancelan
El problema: Zero best practices implementadas.
Las 15 Best Practices No Negociables
1. Testing: Tu Red de Seguridad
Regla de oro: Si no tiene tests, está roto (aún no lo sabes).
// ❌ SIN TESTS: Código frágil
export class PaymentService {
async processPayment(amount: number, accountId: string): Promise<Payment> {
const account = await this.accountRepo.findById(accountId);
if (account.balance < amount) {
throw new InsufficientFundsError();
}
account.balance -= amount;
await this.accountRepo.save(account);
return this.createPayment(amount, accountId);
}
}
// ✅ CON TESTS: Código confiable
describe('PaymentService', () => {
let service: PaymentService;
let accountRepo: MockRepository<Account>;
beforeEach(() => {
accountRepo = createMockRepository();
service = new PaymentService(accountRepo);
});
describe('processPayment', () => {
it('should deduct amount from account balance', async () => {
// Arrange
const account = createTestAccount({ balance: 1000 });
accountRepo.findById.mockResolvedValue(account);
// Act
await service.processPayment(100, account.id);
// Assert
expect(account.balance).toBe(900);
expect(accountRepo.save).toHaveBeenCalledWith(account);
});
it('should throw InsufficientFundsError when balance is low', async () => {
// Arrange
const account = createTestAccount({ balance: 50 });
accountRepo.findById.mockResolvedValue(account);
// Act & Assert
await expect(service.processPayment(100, account.id))
.rejects
.toThrow(InsufficientFundsError);
expect(accountRepo.save).not.toHaveBeenCalled();
});
it('should handle account not found', async () => {
// Arrange
accountRepo.findById.mockResolvedValue(null);
// Act & Assert
await expect(service.processPayment(100, 'invalid-id'))
.rejects
.toThrow(AccountNotFoundError);
});
it('should handle concurrent transactions', async () => {
// Arrange
const account = createTestAccount({ balance: 1000 });
accountRepo.findById.mockResolvedValue(account);
// Act - Simular 2 transacciones simultáneas
const [result1, result2] = await Promise.all([
service.processPayment(600, account.id),
service.processPayment(600, account.id),
]);
// Assert - Una debe fallar
const successful = [result1, result2].filter(r => r.status === 'success');
expect(successful).toHaveLength(1);
});
});
});
Pirámide de Testing:
/\
/ \ E2E (10%)
/____\
/ \ Integration (20%)
/________\
/ \ Unit (70%)
/__________\
Coverage mínimo profesional:
- ✅ Unit tests: 80%+
- ✅ Integration tests: 60%+
- ✅ E2E críticos: 100% de flujos principales
2. Git Flow: Commits que Cuentan Historias
# ❌ MAL: Commits inútiles
git commit -m "fix"
git commit -m "wip"
git commit -m "asdf"
git commit -m "funciona!!!"
# ✅ BIEN: Commits descriptivos (Conventional Commits)
git commit -m "feat(auth): add JWT refresh token rotation"
git commit -m "fix(payments): handle race condition in concurrent transactions"
git commit -m "refactor(user-service): extract validation to separate class"
git commit -m "docs(api): add OpenAPI spec for payment endpoints"
git commit -m "test(orders): add integration tests for order flow"
git commit -m "perf(search): add Redis cache for frequent queries"
git commit -m "chore(deps): upgrade NestJS to v10.3.0"
Convención de commits:
feat: Nueva funcionalidadfix: Corrección de bugsrefactor: Cambio de código sin cambiar funcionalidadtest: Agregar o modificar testsdocs: Documentaciónperf: Mejoras de performancechore: Mantenimiento (deps, configs)ci: Cambios en CI/CD
Branching strategy:
main (production)
├── develop (staging)
│ ├── feature/user-authentication
│ ├── feature/payment-gateway
│ └── feature/admin-dashboard
├── hotfix/critical-payment-bug
└── release/v2.1.0
3. Code Reviews: El Filtro de Calidad
Checklist de code review:
// Pull Request Template
## 📝 Description
Brief description of changes and why they're needed.
## 🎯 Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## ✅ Checklist
- [ ] Code follows style guidelines
- [ ] Self-review completed
- [ ] Comments added for complex logic
- [ ] Documentation updated
- [ ] Tests added/updated
- [ ] All tests passing
- [ ] No console.logs or debuggers
- [ ] No commented code
- [ ] Performance impact considered
## 🧪 Testing
- [ ] Unit tests added
- [ ] Integration tests added
- [ ] Manual testing completed
- [ ] Edge cases covered
## 📸 Screenshots (if UI changes)
## 🔗 Related Issues
Closes #123
Principios de review:
// ❌ Code review negativo
"Este código es horrible, reescríbelo todo"
// ✅ Code review constructivo
/**
* Suggestion: Consider extracting this validation logic
*
* Current approach works, but extracting to a dedicated
* validator class would:
* - Make it reusable across services
* - Easier to test in isolation
* - Follow Single Responsibility Principle
*
* Example:
* ```typescript
* class AccountValidator {
* validateBalance(account: Account, amount: number): void {
* if (account.balance < amount) {
* throw new InsufficientFundsError(account.balance, amount);
* }
* }
* }
* ```
*/
4. CI/CD: Deploy con Confianza
Pipeline de MATIAS IMPULSO:
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
# 1. QUALITY CHECKS
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run lint
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run type-check
# 2. TESTING
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci
- run: npm run test:unit
- run: npm run test:coverage
- name: Upload coverage
uses: codecov/codecov-action@v3
test-integration:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
redis:
image: redis:7
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run test:integration
# 3. SECURITY
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm audit --audit-level=high
- uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
# 4. BUILD
build:
needs: [lint, type-check, test-unit, test-integration]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
# 5. DEPLOY TO STAGING
deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
- name: Deploy to AWS Lambda
run: |
aws lambda update-function-code \
--function-name matias-impulso-staging \
--zip-file fileb://dist.zip
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET }}
- name: Run smoke tests
run: npm run test:smoke -- --env=staging
# 6. DEPLOY TO PRODUCTION
deploy-production:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://matiasimpulso.com
steps:
- uses: actions/download-artifact@v3
- name: Deploy to production (blue)
run: |
aws lambda update-function-code \
--function-name matias-impulso-prod-blue \
--zip-file fileb://dist.zip
- name: Health check blue
run: npm run health-check -- --env=prod-blue
- name: Switch traffic to blue
run: |
aws lambda update-alias \
--function-name matias-impulso-prod \
--name live \
--function-version $BLUE_VERSION
- name: Monitor for 10 minutes
run: npm run monitor -- --duration=10m
- name: Rollback if errors detected
if: failure()
run: |
aws lambda update-alias \
--function-name matias-impulso-prod \
--name live \
--function-version $GREEN_VERSION
Resultados:
- ⚡ Deploy time: 8 minutos (de 45 minutos manual)
- 🛡️ Zero downtime deploys
- 🔄 Rollback automático en <30 segundos
- 📊 Cobertura garantizada: No pasa sin 80%+
5. Logging: Tu Caja Negra
// ❌ Logging amateur
console.log('error');
console.log('user:', user);
// ✅ Logging profesional
import { Logger } from '@nestjs/common';
import { v4 as uuid } from 'uuid';
@Injectable()
export class PaymentService {
private readonly logger = new Logger(PaymentService.name);
async processPayment(request: PaymentRequest): Promise<Payment> {
const correlationId = uuid();
this.logger.log({
message: 'Processing payment',
correlationId,
userId: request.userId,
amount: request.amount,
currency: request.currency,
});
try {
const payment = await this.executePayment(request);
this.logger.log({
message: 'Payment completed successfully',
correlationId,
paymentId: payment.id,
duration: Date.now() - startTime,
});
return payment;
} catch (error) {
this.logger.error({
message: 'Payment failed',
correlationId,
error: error.message,
stack: error.stack,
userId: request.userId,
amount: request.amount,
});
throw error;
}
}
}
// Structured logging con Winston
import winston from 'winston';
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
6. Monitoring: Detecta Problemas Antes que tus Usuarios
// Métricas con Prometheus
import { Counter, Histogram } from 'prom-client';
const paymentCounter = new Counter({
name: 'payments_total',
help: 'Total number of payments',
labelNames: ['status', 'currency'],
});
const paymentDuration = new Histogram({
name: 'payment_duration_seconds',
help: 'Payment processing duration',
buckets: [0.1, 0.5, 1, 2, 5],
});
@Injectable()
export class PaymentService {
async processPayment(request: PaymentRequest): Promise<Payment> {
const timer = paymentDuration.startTimer();
try {
const payment = await this.executePayment(request);
paymentCounter.inc({
status: 'success',
currency: request.currency
});
return payment;
} catch (error) {
paymentCounter.inc({
status: 'failed',
currency: request.currency
});
throw error;
} finally {
timer();
}
}
}
// Alertas en Grafana
// Alert: Payment failure rate > 1%
// Alert: Payment latency p95 > 2s
// Alert: Error rate > 0.5%
7. Error Handling: Falla Graciosamente
// ❌ Error handling amateur
try {
await doSomething();
} catch (e) {
console.log('error');
}
// ✅ Error handling profesional
// 1. Jerarquía de errores custom
export abstract class DomainError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly statusCode: number
) {
super(message);
this.name = this.constructor.name;
}
}
export class InsufficientFundsError extends DomainError {
constructor(
public readonly required: number,
public readonly available: number
) {
super(
`Insufficient funds: required ${required}, available ${available}`,
'INSUFFICIENT_FUNDS',
400
);
}
}
export class PaymentGatewayError extends DomainError {
constructor(
message: string,
public readonly gatewayCode: string
) {
super(message, 'PAYMENT_GATEWAY_ERROR', 502);
}
}
// 2. Global exception filter
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
private readonly logger = new Logger(GlobalExceptionFilter.name);
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
const request = ctx.getRequest();
if (exception instanceof DomainError) {
this.logger.warn({
message: 'Domain error',
code: exception.code,
error: exception.message,
path: request.url,
});
return response.status(exception.statusCode).json({
statusCode: exception.statusCode,
code: exception.code,
message: exception.message,
timestamp: new Date().toISOString(),
path: request.url,
});
}
// Error inesperado
this.logger.error({
message: 'Unexpected error',
error: exception,
stack: exception.stack,
path: request.url,
});
return response.status(500).json({
statusCode: 500,
code: 'INTERNAL_SERVER_ERROR',
message: 'An unexpected error occurred',
timestamp: new Date().toISOString(),
path: request.url,
});
}
}
// 3. Retry con exponential backoff
async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
let lastError: Error;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
lastError = error;
if (attempt < maxRetries - 1) {
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw lastError;
}
// Uso
const payment = await retryWithBackoff(
() => paymentGateway.charge(amount),
3,
1000
);
8. Database Migrations: Evoluciona sin Romper
// TypeORM migrations
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPaymentStatusIndex1700000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// 1. Crear índice de forma concurrente (no bloquea)
await queryRunner.query(`
CREATE INDEX CONCURRENTLY idx_payments_status
ON payments(status)
WHERE status IN ('pending', 'processing')
`);
// 2. Agregar columna con valor default (no requiere rewrite)
await queryRunner.query(`
ALTER TABLE payments
ADD COLUMN idempotency_key VARCHAR(255)
`);
// 3. Backfill de datos existentes en batches
let offset = 0;
const batchSize = 1000;
while (true) {
const result = await queryRunner.query(`
UPDATE payments
SET idempotency_key = md5(id::text || created_at::text)
WHERE idempotency_key IS NULL
LIMIT ${batchSize}
`);
if (result.affectedRows === 0) break;
await new Promise(resolve => setTimeout(resolve, 100)); // Evita saturar DB
}
// 4. Hacer columna NOT NULL después del backfill
await queryRunner.query(`
ALTER TABLE payments
ALTER COLUMN idempotency_key SET NOT NULL
`);
// 5. Agregar índice único
await queryRunner.query(`
CREATE UNIQUE INDEX CONCURRENTLY idx_payments_idempotency
ON payments(idempotency_key)
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX idx_payments_idempotency`);
await queryRunner.query(`ALTER TABLE payments DROP COLUMN idempotency_key`);
await queryRunner.query(`DROP INDEX idx_payments_status`);
}
}
9. API Versioning: Evoluciona sin Romper Clientes
// ❌ Sin versionado: Breaking changes
@Controller('users')
export class UserController {
@Get(':id')
async getUser(@Param('id') id: string) {
return { id, name: 'Lewis' }; // Cambiamos estructura = clientes rotos
}
}
// ✅ Con versionado: Backward compatibility
@Controller({ path: 'users', version: '1' })
export class UserControllerV1 {
@Get(':id')
async getUser(@Param('id') id: string): Promise<UserV1Response> {
const user = await this.userService.getUser(id);
return { id: user.id, name: user.name }; // Formato original
}
}
@Controller({ path: 'users', version: '2' })
export class UserControllerV2 {
@Get(':id')
async getUser(@Param('id') id: string): Promise<UserV2Response> {
const user = await this.userService.getUser(id);
return {
id: user.id,
profile: {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
},
metadata: {
createdAt: user.createdAt,
updatedAt: user.updatedAt,
},
};
}
}
// Cliente decide qué versión usar
// GET /v1/users/123 -> Formato viejo
// GET /v2/users/123 -> Formato nuevo
10. Documentation: Código que se Explica Solo (Casi)
/**
* Procesa un pago bancario con validaciones completas y retry automático
*
* @param request - Datos del pago a procesar
* @param request.amount - Monto en centavos (ej: 10000 = $100.00)
* @param request.currency - Código ISO de moneda (USD, EUR, COP)
* @param request.accountId - ID de la cuenta a debitar
*
* @returns Payment procesado con ID de transacción
*
* @throws {InsufficientFundsError} Cuando la cuenta no tiene fondos suficientes
* @throws {AccountNotFoundError} Cuando la cuenta no existe
* @throws {InvalidAmountError} Cuando el monto es <= 0 o > límite diario
* @throws {PaymentGatewayError} Cuando el gateway externo falla después de 3 intentos
*
* @example
* ```typescript
* const payment = await paymentService.processPayment({
* amount: 50000, // $500.00
* currency: 'USD',
* accountId: 'acc_123',
* });
* console.log(payment.id); // "pmt_xyz"
* ```
*
* @see https://docs.matiasimpulso.com/payments/processing
*/
async processPayment(request: PaymentRequest): Promise<Payment> {
// Implementation
}
// OpenAPI/Swagger
@ApiTags('payments')
@Controller('payments')
export class PaymentController {
@Post()
@ApiOperation({
summary: 'Procesar pago',
description: 'Procesa un pago bancario con validaciones y retry automático'
})
@ApiResponse({
status: 201,
description: 'Pago procesado exitosamente',
type: PaymentResponse
})
@ApiResponse({
status: 400,
description: 'Fondos insuficientes o monto inválido'
})
@ApiResponse({
status: 404,
description: 'Cuenta no encontrada'
})
@ApiResponse({
status: 502,
description: 'Error del gateway de pagos'
})
async processPayment(
@Body() dto: ProcessPaymentDto
): Promise<PaymentResponse> {
return this.paymentService.processPayment(dto);
}
}
Caso Real: Best Practices en Banco Estado Chile
Contexto: APIs críticas con SLA 99.99%, 10K TPS.
Best practices implementadas:
- Testing: 92% coverage (unit + integration)
- CI/CD: 8 deploys diarios sin downtime
- Monitoring: Grafana + Prometheus + alertas 24/7
- Logging: Structured JSON logs con correlation IDs
- Error handling: Retry automático con circuit breaker
- Database: Migrations con zero-downtime strategy
- API: Versionado + backward compatibility
- Security: Snyk + SonarQube + penetration testing
- Documentation: OpenAPI + ejemplos + Postman collections
- Code reviews: Mandatory, 2 approvals mínimo
Resultados:
- ✅ Uptime: 99.98% (mejor que SLA)
- ✅ MTTR: 4 minutos (de 45 minutos)
- ✅ Bugs en prod: -85%
- ✅ Deploy confidence: 100%
Checklist: ¿Tu Proyecto es Profesional?
🧪 Testing
- Coverage > 80% (unit + integration)
- Tests corren en CI/CD
- Tests son rápidos (<5 min total)
- Tests no son flaky
🔄 CI/CD
- Pipeline automatizado completo
- Deploy automático a staging
- Approval manual para production
- Rollback automático si falla health check
- Zero downtime deploys
📊 Monitoring
- Métricas de negocio (payments, users, etc.)
- Métricas técnicas (latency, errors, throughput)
- Alertas configuradas (<5 min response time)
- Dashboards en tiempo real
📝 Logging
- Structured logging (JSON)
- Correlation IDs en todas las requests
- Log levels apropiados (ERROR, WARN, INFO, DEBUG)
- Logs centralizados (CloudWatch, Datadog, etc.)
🐛 Error Handling
- Errores custom por dominio
- Global exception filter
- Retry con exponential backoff
- Circuit breaker para servicios externos
📚 Documentation
- README con setup instructions
- API documentation (OpenAPI/Swagger)
- Arquitectura documentada (diagrams)
- Ejemplos de código actualizados
🔒 Security
- Dependencias escaneadas (Snyk, npm audit)
- Secrets en variables de entorno (nunca en código)
- HTTPS everywhere
- Rate limiting configurado
- Input validation en todos los endpoints
🗄️ Database
- Migrations versionadas
- Backups automáticos
- Índices en queries frecuentes
- Connection pooling configurado
Herramientas del Arsenal Profesional
{
"devDependencies": {
// Testing
"jest": "^29.7.0",
"@testing-library/react": "^14.0.0",
"supertest": "^6.3.0",
// Quality
"eslint": "^8.50.0",
"prettier": "^3.0.0",
"husky": "^8.0.0",
"lint-staged": "^14.0.0",
// Type Safety
"typescript": "^5.3.0",
"@types/node": "^20.10.0",
// Security
"snyk": "^1.1200.0",
// Documentation
"@nestjs/swagger": "^7.1.0",
// Monitoring
"prom-client": "^15.0.0",
"winston": "^3.11.0"
}
}
Conclusión
Best practices no son burocracia. Son la diferencia entre software amateur y software profesional.
Los mejores equipos:
- 🧪 Testean todo antes de producción
- 🔄 Automatizan lo repetitivo
- 📊 Miden antes de optimizar
- 📝 Documentan para su yo futuro
- 🛡️ Previenen en vez de apagar incendios
Empieza con una práctica. Domínala. Agrega la siguiente.
No intentes implementar todo de golpe. En MATIAS IMPULSO empezamos con:
- Mes 1: Tests básicos + CI/CD simple
- Mes 2: Logging estructurado + monitoring básico
- Mes 3: Error handling robusto + alertas
- Mes 4: Documentation completa + security scans
Hoy tenemos un sistema que funciona 24/7 sin supervisión constante.
Ese es el poder de las best practices.
Lewis Lopez - 10+ años aplicando best practices en producción
Zero incidentes críticos en 18 meses
Fundador de MATIAS IMPULSO
¿Consultas sobre implementación? contacto@lewislopez.io
Temas relacionados:
¿Te gustó este artículo?
Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.