Produccion lista

LeadFlow CRM — Backend API

Arquitectura Node.js 20 · TypeScript · Fastify · PostgreSQL · Redis — generada con patrones-backend-nodejs

Node.js 20 TypeScript 5 Fastify 4 PostgreSQL 16 Redis 7 JWT Auth Zod Pino
LeadFlow CRM API
SaaS B2B · Gestion de leads para agencias de marketing digital
🔌
Framework
Fastify 4
~65k req/s en benchmark
🗄
Base de datos
PostgreSQL 16
Pool de 10 conexiones
Cache
Redis 7
Rate limit + cache
🔐
Auth
JWT dual
Access 15m · Refresh 7d
📊
Logging
Pino
Structured JSON logs
🛡
Validacion
Zod
Schemas tipados
🔀
Arquitectura en Capas
Flujo de una peticion HTTP a traves del sistema
HTTP / Middleware
Fastify · Helmet · CORS
Rate Limit · Pino logger
Controller
lead.controller.ts
auth.controller.ts
Service
lead.service.ts
auth.service.ts
Repository
lead.repository.ts
user.repository.ts
Database
PostgreSQL + Redis
pg Pool · ioredis
🗂
Estructura del Proyecto
leadflow-api/src/
leadflow-api/
src/
config/
database.ts— Pool PostgreSQL + config Redis
env.ts— Variables con Zod schema validation
controllers/
lead.controller.ts— CRUD leads (HTTP in/out)
auth.controller.ts— Login, refresh, logout
health.controller.ts— /health endpoint
services/
lead.service.ts— Logica de negocio de leads
auth.service.ts— JWT generation + bcrypt
repositories/
lead.repository.ts— SQL queries con pg Pool
user.repository.ts— Auth user persistence
middleware/
auth.middleware.ts— JWT verify + inject user
validate.middleware.ts— Zod schema parse
rate-limit.middleware.ts— Redis-backed limiter
error-handler.ts— Global error boundary
routes/
lead.routes.ts— /api/v1/leads
auth.routes.ts— /api/v1/auth
types/
lead.types.ts— Lead, CreateLeadDTO, UpdateLeadDTO
auth.types.ts— JWTPayload, Tokens
utils/
errors.ts— AppError hierarchy
response.ts— ApiResponse helper
server.ts— Fastify bootstrap + graceful shutdown
app.ts— Plugin registration
tsconfig.json package.json .env.example
📋
Endpoints REST
Base URL: https://api.leadflow.io/api/v1
Metodo Ruta Descripcion Auth
POST /auth/login Login con email + password → tokens JWT Publico
POST /auth/refresh Renovar access token con refresh token Publico
POST /auth/logout Invalidar refresh token en Redis JWT
GET /leads Listar leads con paginacion (page, limit, status) JWT
GET /leads/:id Obtener lead por ID JWT
POST /leads Crear nuevo lead con validacion Zod JWT
PUT /leads/:id Actualizar lead (status, asignacion, notas) JWT
DELETE /leads/:id Eliminar lead (soft delete) JWT
GET /health Estado del servidor, DB y Redis Publico
🔀
Middleware Pipeline
Cada peticion autenticada pasa por este stack en orden
1
Helmet + CORS
Cabeceras de seguridad, origenes permitidos desde .env
@fastify/helmet · @fastify/cors
2
Request Logger
Pino log: method, url, IP, user-agent
pino · pino-pretty
3
Rate Limiter
100 req/15min (API) · 5 req/15min (auth)
@fastify/rate-limit · Redis store
4
JWT Authenticate
Extrae Bearer token, verifica firma, inyecta req.user
jsonwebtoken · 401 si invalido
5
Zod Validation
Body/query/params contra schema tipado
zod · 400 con field errors
6
Route Handler
Controller → Service → Repository → DB
asyncHandler wrapper
7
Global Error Handler
AppError → statusCode · Unknown → 500 sin leak en produccion
errorHandler middleware
⚙️
Codigo Clave: Lead Service
Logica de negocio con validacion, asignacion y eventos
src/services/lead.service.ts
TypeScript
import { LeadRepository } from '../repositories/lead.repository'; import { CreateLeadDTO, UpdateLeadDTO, Lead, LeadStatus } from '../types/lead.types'; import { NotFoundError, ValidationError, ConflictError } from '../utils/errors'; import { CacheService } from '../utils/cache'; export class LeadService { constructor( private leadRepo: LeadRepository, private cache: CacheService, ) {} async createLead(data: CreateLeadDTO, agencyId: string): Promise<Lead> { // Verificar duplicado por email dentro de la agencia const existing = await this.leadRepo.findByEmail(data.email, agencyId); if (existing) throw new ConflictError(`Lead con email ${data.email} ya existe`); const lead = await this.leadRepo.create({ ...data, agencyId, status: LeadStatus.NEW, score: this.calculateScore(data), }); // Invalidar cache de listado await this.cache.invalidatePattern(`leads:${agencyId}:*`); return lead; } async getLeads(agencyId: string, opts: { page: number; limit: number; status?: LeadStatus }) { const cacheKey = `leads:${agencyId}:${opts.page}:${opts.limit}:${opts.status ?? 'all'}`; const cached = await this.cache.get<Lead[]>(cacheKey); if (cached) return cached; const result = await this.leadRepo.findMany({ agencyId, ...opts }); await this.cache.set(cacheKey, result, 300); // TTL 5 min return result; } async assignLead(leadId: string, agentId: string): Promise<Lead> { const lead = await this.leadRepo.findById(leadId); if (!lead) throw new NotFoundError('Lead no encontrado'); if (lead.status === LeadStatus.CLOSED) throw new ValidationError('No se puede asignar un lead cerrado'); return this.leadRepo.update(leadId, { assignedTo: agentId, status: LeadStatus.ASSIGNED }); } private calculateScore(data: CreateLeadDTO): number { let score = 0; if (data.phone) score += 20; if (data.company) score += 30; if (data.budget && data.budget > 5000) score += 40; if (data.source === 'referral') score += 10; return score; } }
src/utils/errors.ts
TypeScript
export class AppError extends Error { constructor( public message: string, public statusCode: number = 500, public isOperational: boolean = true, ) { super(message); Object.setPrototypeOf(this, AppError.prototype); Error.captureStackTrace(this, this.constructor); } } export class ValidationError extends AppError { constructor(message: string, public errors?: any[]) { super(message, 400); } } export class NotFoundError extends AppError { constructor(m = 'Not found') { super(m, 404); } } export class UnauthorizedError extends AppError { constructor(m = 'Unauthorized') { super(m, 401); } } export class ForbiddenError extends AppError { constructor(m = 'Forbidden') { super(m, 403); } } export class ConflictError extends AppError { constructor(m: string) { super(m, 409); } }
src/server.ts
TypeScript
import Fastify from 'fastify'; import helmet from '@fastify/helmet'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import { createPool } from './config/database'; import { leadRoutes } from './routes/lead.routes'; import { authRoutes } from './routes/auth.routes'; const app = Fastify({ logger: { level: process.env.LOG_LEVEL || 'info' }, }); async function bootstrap() { const db = await createPool(); await app.register(helmet); await app.register(cors, { origin: process.env.ALLOWED_ORIGINS?.split(',') }); await app.register(rateLimit, { max: 100, timeWindow: '15 minutes' }); await app.register(leadRoutes, { prefix: '/api/v1', db }); await app.register(authRoutes, { prefix: '/api/v1', db }); // Graceful shutdown const shutdown = async () => { app.log.info('Shutting down...'); await app.close(); await db.end(); process.exit(0); }; process.on('SIGTERM', shutdown); process.on('SIGINT', shutdown); await app.listen({ port: 3000, host: '0.0.0.0' }); } bootstrap().catch(console.error);
Checklist de Produccion
Todos los puntos implementados en este backend