Skill CULTIVA IA · IA-Ingeniería-MLOps
NestJS 10 TypeScript 5 Producción

LeadFlow AI — Arquitectura NestJS

Backend modular de producción para cualificación de leads con IA. API REST, JWT, DTOs validados, queues BullMQ y configuración por entorno.

Ciclo de vida de una petición
01
HTTP Request
POST /leads
02
Guards
JwtAuthGuard → RolesGuard
03
ValidationPipe
CreateLeadDto whitelist
04
Controller
LeadsController.create()
05
Service
LeadsService → Queue
06
Interceptor
ClassSerializer → Response
leadflow-api / src
src/
main.tsbootstrap + GlobalPipes
app.module.ts
common/transversal
filters/
http-exception.filter.ts
guards/
jwt-auth.guard.ts
roles.guard.ts
interceptors/
logging.interceptor.ts
decorators/
roles.decorator.ts
config/
configuration.ts
validation.tsvalida env al boot
modules/
auth/JWT + strategies
leads/ingesta + CRUD
dto/
entities/
scoring/BullMQ worker
webhooks/ingesta externa
analytics/métricas internas
prisma/
prisma.service.ts
*.spec.tstests unitarios
Módulos de dominio

Auth Module

auth.controller.ts — /auth/login, /auth/refresh
auth.service.ts — bcrypt + JWT sign
jwt.strategy.ts — extrae user del token
dto/login.dto.ts — @IsEmail, @IsString

Leads Module

leads.controller.ts — POST /leads, GET /leads/:id
leads.service.ts — persiste + encola scoring
dto/create-lead.dto.ts — validated DTO
entities/lead.entity.ts — Prisma model

Scoring Module (BullMQ)

scoring.processor.ts — @Processor('scoring')
scoring.service.ts — llama al modelo IA
scoring.module.ts — BullModule.registerQueue()

Webhooks + Analytics

webhooks.controller.ts — valida HMAC signature
analytics.service.ts — agrega métricas scoring
Patrones clave — código de producción
📄 src/main.ts TypeScript
async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bufferLogs: true,
  });

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
      transformOptions: { enableImplicitConversion: true },
    }),
  );

  app.useGlobalInterceptors(
    new ClassSerializerInterceptor(app.get(Reflector)),
  );
  app.useGlobalFilters(new HttpExceptionFilter());

  // Rate limiting en endpoints públicos
  app.use(rateLimit({ windowMs: 60_000, max: 100 }));

  await app.listen(
    process.env.PORT ?? 3000
  );
}
bootstrap();
📄 dto/create-lead.dto.ts TypeScript
export enum LeadSource {
  WEB    = 'web',
  CRM    = 'crm',
  LINKEDIN = 'linkedin',
}

export class CreateLeadDto {
  @IsEmail()
  email: string;

  @IsString()
  @Length(2, 80)
  fullName: string;

  @IsString()
  @IsOptional()
  company?: string;

  @IsEnum(LeadSource)
  source: LeadSource;

  @IsObject()
  @IsOptional()
  metadata?: Record<string, unknown>;
}
📄 leads/leads.controller.ts TypeScript
@Controller('leads')
@UseGuards(JwtAuthGuard, RolesGuard)
export class LeadsController {
  constructor(
    private readonly leadsService: LeadsService,
  ) {}

  @Post()
  @Roles('admin', 'analyst')
  @HttpCode(202)
  async create(
    @Body() dto: CreateLeadDto,
    @Req() req: AuthenticatedRequest,
  ) {
    return this.leadsService.ingest(dto, req.user.id);
  }

  @Get(':id')
  findOne(
    @Param('id', ParseUUIDPipe) id: string
  ) {
    return this.leadsService.findById(id);
  }
}
📄 common/filters/http-exception.filter.ts TypeScript
@Catch()
export class HttpExceptionFilter
  implements ExceptionFilter {

  catch(exception: unknown, host: ArgumentsHost) {
    const res = host.switchToHttp()
      .getResponse<Response>();
    const req = host.switchToHttp()
      .getRequest<Request>();

    if (exception instanceof HttpException) {
      return res.status(exception.getStatus()).json({
        path:  req.url,
        ts:    new Date().toISOString(),
        error: exception.getResponse(),
      });
    }

    return res.status(500).json({
      path:  req.url,
      ts:    new Date().toISOString(),
      error: 'Internal server error',
    });
  }
}
📄 scoring/scoring.processor.ts — BullMQ Worker TypeScript
@Processor('scoring')
export class ScoringProcessor extends WorkerHost {
  constructor(private readonly scoringService: ScoringService) { super(); }

  async process(job: Job<ScoringJobDto>): Promise<void> {
    const { leadId, features } = job.data;
    const score = await this.scoringService.predict(features);   // llamada al modelo IA
    await this.scoringService.saveScore(leadId, score);            // persiste en Prisma
  }
}

// En LeadsService — encola tras persistir
async ingest(dto: CreateLeadDto, userId: string) {
  const lead = await this.prisma.lead.create({ data: { ...dto, userId } });
  await this.scoringQueue.add('score-lead', { leadId: lead.id, features: dto.metadata }, {
    attempts: 3, backoff: { type: 'exponential', delay: 2000 },
  });
  return { id: lead.id, status: 'queued' };
}
Variables de entorno — validadas al arranque
Variable Valor ejemplo Descripción Req.
DATABASE_URL postgresql://leadflow:pass@db:5432/leadflow PostgreSQL connection string (Prisma) Required
JWT_SECRET ***32-char-random*** Firma de tokens JWT (RS256 en prod) Required
JWT_EXPIRES_IN 15m Expiración del access token Required
REDIS_URL redis://redis:6379 BullMQ queue + caché de sesión Required
AI_MODEL_ENDPOINT https://ml.leadflow.ai/predict Endpoint del modelo de scoring IA Required
AI_MODEL_API_KEY lf_*** API key interna del servicio ML Required
WEBHOOK_HMAC_SECRET *** Valida firma HMAC-SHA256 en webhooks Required
PORT 3000 Puerto de escucha del servidor HTTP Optional
LOG_LEVEL info Nivel de logs estructurados (pino) Optional
Checklist de producción — LeadFlow AI API
ValidationPipe global con whitelist Aplicado en bootstrap — rechaza campos extra en todos los DTOs
ClassSerializerInterceptor global Oculta campos @Exclude() (passwords, tokens) en todas las respuestas
HttpExceptionFilter centralizado Envelope consistente {path, ts, error} en todos los errores
Env validada al arranque con class-validator El proceso muere si falta DATABASE_URL, JWT_SECRET o REDIS_URL
Scoring asíncrono con BullMQ Ingesta responde 202 inmediatamente; scoring en worker con reintentos
Rate limiting en endpoints públicos 100 req/min por IP en POST /leads y endpoints de webhooks
Autenticación JWT + roles JwtAuthGuard + RolesGuard + @Roles() decorator en cada endpoint
Repositorio Prisma aislado en módulo Controladores nunca coordinan escrituras multi-paso directamente
Tests con ValidationPipe idéntico al de prod createTestingModule() aplica los mismos pipes/filters que main.ts
!
Health checks en /health Pendiente: @nestjs/terminus con DB + Redis + modelo IA
!
Correlation IDs en logs Pendiente: middleware para añadir x-request-id en pino logger
!
OpenAPI / Swagger auto-generado Pendiente: @nestjs/swagger con SwaggerModule.setup()
Principios de arquitectura aplicados
🏗️

Módulos por dominio

Cada feature vive en su módulo: leads, scoring, webhooks, analytics. Cero cruce de capas.

🛡️

Controllers delgados

Solo parsean input HTTP y devuelven respuesta. La lógica vive en los servicios.

DTOs + whitelist

Validación con class-validator y whitelist:true. Ningún campo no declarado pasa.

🔒

Guards jerárquicos

JwtAuthGuard primero, luego RolesGuard. Autorización de recurso en el service.

Queues asíncronos

Scoring pesado en BullMQ. La API responde 202 y el worker procesa con reintentos.

🌍

Config validada

ConfigModule carga y valida todas las variables de entorno antes del primer request.

🗄️

Repositorio aislado

Prisma solo se toca en servicios. Las transacciones multi-paso no escalan a controladores.

🧪

Tests realistas

createTestingModule replica pipes/filtros de producción. Los guards se testean con requests HTTP.