DEV Community

Pedro Alvarado
Pedro Alvarado

Posted on

Semana 2 — NestJS: módulos, controllers, providers

NestJS para Desarrolladores Rails

De ApplicationController a Arquitectura Modular

Tiempo estimado: 4-6 horas | Nivel: Intermedio | Prerrequisitos: Rails, TypeScript básico


📋 Índice


🎯 Objetivo y Quick Start

Objetivo: Dominar NestJS aprovechando tu experiencia en Rails, entendiendo las diferencias arquitecturales y adoptando el mindset de inyección de dependencias.

Quick Start (5 minutos)

# Instalar NestJS CLI
npm i -g @nestjs/cli

# Crear proyecto
nest new blog-api
cd blog-api

# Instalar dependencias esenciales
npm install class-validator class-transformer

# Generar recurso completo
nest g resource posts

# Ejecutar
npm run start:dev
Enter fullscreen mode Exit fullscreen mode

Checkpoint: Visita http://localhost:3000/posts - deberías ver un JSON vacío.


🏗️ Arquitectura: Rails vs NestJS

Comparación mental

Rails NestJS Propósito
ApplicationController AppModule Punto de entrada de la app
Controller Controller Manejar requests HTTP
Service Objects/POROs Providers/Services Lógica de negocio
before_action Guards Autenticación/Autorización
around_action Interceptors Middleware personalizado
Strong Parameters DTOs + Validation Validación de entrada
Initializers Modules Configuración y dependencias

Flujo de request

graph LR
    A[HTTP Request] --> B[Guard] 
    B --> C[Interceptor Before]
    C --> D[Controller]
    D --> E[Service]
    E --> F[Interceptor After]
    F --> G[HTTP Response]
Enter fullscreen mode Exit fullscreen mode

En Rails: Request → before_action → Controller → Service → around_action → Response

En NestJS: Request → Guard → Interceptor → Controller → Service → Interceptor → Response

Diferencias clave

Rails (Convention over Configuration):

# Todo implícito, basado en convenciones
class PostsController < ApplicationController
  def create
    @post = PostService.new.create(post_params) # Instanciación manual
  end
end
Enter fullscreen mode Exit fullscreen mode

NestJS (Explicit Dependencies):

// Todo explícito, basado en decoradores
@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {} // DI automática

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

⚡ Setup y primer recurso

Estructura del proyecto

src/
├── app.controller.ts      # Como routes.rb + ApplicationController
├── app.module.ts          # Como config/application.rb
├── app.service.ts         # Service base
├── main.ts               # Como config/boot.rb
└── posts/                # Módulo generado
    ├── dto/
    │   ├── create-post.dto.ts
    │   └── update-post.dto.ts
    ├── posts.controller.ts
    ├── posts.module.ts
    ├── posts.service.ts
    └── entities/
        └── post.entity.ts
Enter fullscreen mode Exit fullscreen mode

Anatomía de los archivos generados

posts.module.ts (Como un Rails Engine):

@Module({
  controllers: [PostsController], // Registra controllers
  providers: [PostsService],      // Registra services para DI
  exports: [PostsService]         // Hace service disponible para otros módulos
})
export class PostsModule {}
Enter fullscreen mode Exit fullscreen mode

posts.controller.ts (Familiar, pero con decoradores):

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()           // Equivale a: post '/posts'
  @Get()            // Equivale a: get '/posts'  
  @Get(':id')       // Equivale a: get '/posts/:id'
  @Patch(':id')     // Equivale a: patch '/posts/:id'
  @Delete(':id')    // Equivale a: delete '/posts/:id'
}
Enter fullscreen mode Exit fullscreen mode

posts.service.ts (Como un Service Object):

@Injectable() // Marca la clase como inyectable
export class PostsService {
  private posts: Post[] = []; // En Rails sería un modelo/repositorio

  create(createPostDto: CreatePostDto): Post {
    const post = { id: Date.now(), ...createPostDto };
    this.posts.push(post);
    return post;
  }
}
Enter fullscreen mode Exit fullscreen mode

🔍 Inyección de dependencias

Rails vs NestJS: Gestión de dependencias

Rails (Manual y implícita):

class PostsController < ApplicationController
  def initialize
    @post_service = PostService.new
    @email_service = EmailService.new
    @logger = Rails.logger
  end
end
Enter fullscreen mode Exit fullscreen mode

NestJS (Automática y explícita):

@Controller('posts')
export class PostsController {
  constructor(
    private readonly postsService: PostsService,
    private readonly emailService: EmailService,
    private readonly logger: Logger
  ) {}
  // NestJS inyecta automáticamente las dependencias
}
Enter fullscreen mode Exit fullscreen mode

Tipos de providers

1. Valor constante:

@Module({
  providers: [
    {
      provide: 'API_KEY',
      useValue: process.env.API_KEY
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

2. Factory (como Rails initializer):

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: () => {
        return new DatabaseConnection(process.env.DATABASE_URL);
      }
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

3. Clase alternativa:

@Module({
  providers: [
    {
      provide: PostsService,
      useClass: MockPostsService // Para testing
    }
  ]
})
Enter fullscreen mode Exit fullscreen mode

Inyección personalizada

@Injectable()
export class PostsService {
  constructor(
    @Inject('API_KEY') private readonly apiKey: string,
    @Inject('DATABASE_CONNECTION') private readonly db: any
  ) {}
}
Enter fullscreen mode Exit fullscreen mode

✅ DTOs y validación avanzada

Rails Strong Parameters vs NestJS DTOs

Rails:

def post_params
  params.require(:post).permit(:title, :body, tags: [])
end
Enter fullscreen mode Exit fullscreen mode

NestJS:

export class CreatePostDto {
  @IsString()
  @Length(3, 120)
  title: string;

  @IsString()
  @Length(1, 10000)  
  body: string;

  @IsArray()
  @IsString({ each: true })
  @ArrayMinSize(1)
  tags: string[];
}
Enter fullscreen mode Exit fullscreen mode

Configuración global de validación

main.ts:

import { ValidationPipe } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Equivale a configurar strong parameters globalmente
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,              // Solo permite propiedades del DTO
    forbidNonWhitelisted: true,   // Rechaza propiedades extra
    transform: true,              // Convierte tipos automáticamente
    disableErrorMessages: false   // En prod, cambiar a true
  }));

  await app.listen(3000);
}
Enter fullscreen mode Exit fullscreen mode

Validaciones avanzadas

Validaciones condicionales:

export class CreatePostDto {
  @IsString()
  title: string;

  @IsString()
  body: string;

  @IsOptional()
  @IsDateString()
  @ValidateIf(o => o.status === 'scheduled')
  publishedAt?: string;

  @IsIn(['draft', 'published', 'scheduled'])
  status: string;
}
Enter fullscreen mode Exit fullscreen mode

Validaciones personalizadas:

import { registerDecorator, ValidationOptions } from 'class-validator';

export function IsNotProfane(validationOptions?: ValidationOptions) {
  return function (object: Object, propertyName: string) {
    registerDecorator({
      name: 'isNotProfane',
      target: object.constructor,
      propertyName: propertyName,
      options: validationOptions,
      validator: {
        validate(value: any) {
          const profaneWords = ['spam', 'bad'];
          return typeof value === 'string' && !profaneWords.some(word => 
            value.toLowerCase().includes(word)
          );
        },
        defaultMessage() {
          return 'El contenido contiene palabras no permitidas';
        }
      },
    });
  };
}

export class CreatePostDto {
  @IsString()
  @IsNotProfane({ message: 'El título no puede contener contenido inapropiado' })
  title: string;
}
Enter fullscreen mode Exit fullscreen mode

Transformación de datos:

import { Transform } from 'class-transformer';

export class CreatePostDto {
  @IsString()
  @Transform(({ value }) => value.trim().toLowerCase())
  title: string;

  @IsArray()
  @Transform(({ value }) => value.filter(tag => tag.trim().length > 0))
  tags: string[];
}
Enter fullscreen mode Exit fullscreen mode

Manejo de errores de validación

// custom-validation.pipe.ts
@Injectable()
export class CustomValidationPipe extends ValidationPipe {
  public createExceptionFactory() {
    return (validationErrors: ValidationError[] = []) => {
      const errors = this.flattenValidationErrors(validationErrors);
      return new BadRequestException({
        message: 'Validation failed',
        errors: errors,
        timestamp: new Date().toISOString(),
      });
    };
  }

  private flattenValidationErrors(validationErrors: ValidationError[]): any {
    return validationErrors.reduce((acc, error) => {
      acc[error.property] = Object.values(error.constraints);
      return acc;
    }, {});
  }
}
Enter fullscreen mode Exit fullscreen mode

🛡️ Guards, Interceptors y Middleware

Guards: Como before_action pero más potente

Rails before_action:

class ApplicationController < ActionController::Base
  before_action :authenticate_user!
  before_action :check_api_key, only: [:api_method]
end
Enter fullscreen mode Exit fullscreen mode

NestJS Guard:

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;

    // Lógica de validación
    return this.validateToken(token);
  }

  private validateToken(token: string): boolean {
    return token === `Bearer ${process.env.API_TOKEN}`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Uso del Guard:

@Controller('posts')
@UseGuards(AuthGuard) // Aplica a todo el controller
export class PostsController {

  @Post()
  @UseGuards(AdminGuard) // Aplica solo a este endpoint
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Guards avanzados con inyección de dependencias

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(
    private readonly usersService: UsersService,
    private readonly reflector: Reflector
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) return true;

    const request = context.switchToHttp().getRequest();
    const user = await this.usersService.findByToken(request.headers.authorization);

    return requiredRoles.some(role => user?.roles?.includes(role));
  }
}

// Custom decorator
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// Uso
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
  @Post('posts')
  @Roles('admin', 'editor')
  createPost() {
    // Solo admin y editor pueden acceder
  }
}
Enter fullscreen mode Exit fullscreen mode

Interceptors: Como around_action pero más versátil

Rails around_action:

around_action :measure_time

private

def measure_time
  start_time = Time.current
  yield
  Rails.logger.info "Request took #{Time.current - start_time}s"
end
Enter fullscreen mode Exit fullscreen mode

NestJS Interceptor:

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;
    const start = Date.now();

    console.log(`→ ${method} ${url}`);

    return next.handle().pipe(
      tap((data) => {
        console.log(`← ${method} ${url} - ${Date.now() - start}ms`);
      }),
      catchError((error) => {
        console.log(`✗ ${method} ${url} - ERROR: ${error.message}`);
        throw error;
      })
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Interceptor de transformación de respuesta:

@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor<T, any> {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    return next.handle().pipe(
      map(data => ({
        success: true,
        data,
        timestamp: new Date().toISOString(),
        path: context.switchToHttp().getRequest().url
      }))
    );
  }
}

// Respuesta transformada:
// {
//   "success": true,
//   "data": { "id": 1, "title": "Mi post" },
//   "timestamp": "2025-01-20T10:30:00.000Z",
//   "path": "/posts"
// }
Enter fullscreen mode Exit fullscreen mode

Middleware global

// logger.middleware.ts
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl } = req;
    const start = Date.now();

    res.on('finish', () => {
      const { statusCode } = res;
      const duration = Date.now() - start;
      console.log(`${method} ${originalUrl} ${statusCode} - ${duration}ms`);
    });

    next();
  }
}

// app.module.ts
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*'); // Aplica a todas las rutas
  }
}
Enter fullscreen mode Exit fullscreen mode

Orden de ejecución

Request
  ↓
Middleware Global
  ↓  
Guards
  ↓
Interceptors (before)
  ↓
Pipes (validation)
  ↓
Controller
  ↓
Service
  ↓
Interceptors (after)
  ↓
Response
Enter fullscreen mode Exit fullscreen mode

🧪 Testing completo

Configuración de testing

Rails: rails_helper.rb + spec_helper.rb
NestJS: Jest configurado automáticamente

Tests unitarios de servicios

// posts.service.spec.ts
describe('PostsService', () => {
  let service: PostsService;
  let mockRepository: jest.Mocked<PostsRepository>;

  beforeEach(async () => {
    const mockRepo = {
      save: jest.fn(),
      find: jest.fn(),
      findOne: jest.fn(),
      delete: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      providers: [
        PostsService,
        { provide: PostsRepository, useValue: mockRepo }
      ],
    }).compile();

    service = module.get<PostsService>(PostsService);
    mockRepository = module.get(PostsRepository);
  });

  describe('create', () => {
    it('should create and return a post', async () => {
      const createPostDto = { title: 'Test Post', body: 'Content' };
      const expectedPost = { id: 1, ...createPostDto };

      mockRepository.save.mockResolvedValue(expectedPost);

      const result = await service.create(createPostDto);

      expect(mockRepository.save).toHaveBeenCalledWith(createPostDto);
      expect(result).toEqual(expectedPost);
    });

    it('should throw error if title is empty', async () => {
      const createPostDto = { title: '', body: 'Content' };

      await expect(service.create(createPostDto)).rejects.toThrow(
        'Title cannot be empty'
      );
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Tests de integración de controladores

// posts.controller.spec.ts
describe('PostsController', () => {
  let controller: PostsController;
  let service: PostsService;

  beforeEach(async () => {
    const mockService = {
      create: jest.fn(),
      findAll: jest.fn(),
      findOne: jest.fn(),
    };

    const module: TestingModule = await Test.createTestingModule({
      controllers: [PostsController],
      providers: [{ provide: PostsService, useValue: mockService }],
    }).compile();

    controller = module.get<PostsController>(PostsController);
    service = module.get<PostsService>(PostsService);
  });

  describe('create', () => {
    it('should create a post', async () => {
      const createPostDto = { title: 'Test', body: 'Content' };
      const expectedResult = { id: 1, ...createPostDto };

      jest.spyOn(service, 'create').mockResolvedValue(expectedResult);

      const result = await controller.create(createPostDto);

      expect(service.create).toHaveBeenCalledWith(createPostDto);
      expect(result).toBe(expectedResult);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Tests E2E con supertest

// app.e2e-spec.ts
describe('PostsController (e2e)', () => {
  let app: INestApplication;

  beforeEach(async () => {
    const moduleFixture: TestingModule = await Test.createTestingModule({
      imports: [AppModule],
    }).compile();

    app = moduleFixture.createNestApplication();

    // Aplicar los mismos pipes que en producción
    app.useGlobalPipes(new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
    }));

    await app.init();
  });

  afterAll(async () => {
    await app.close();
  });

  describe('POST /posts', () => {
    it('should create a post', () => {
      return request(app.getHttpServer())
        .post('/posts')
        .send({ title: 'Test Post', body: 'Test content' })
        .expect(201)
        .expect((res) => {
          expect(res.body.title).toBe('Test Post');
          expect(res.body.id).toBeDefined();
        });
    });

    it('should validate required fields', () => {
      return request(app.getHttpServer())
        .post('/posts')
        .send({ title: 'Hi' }) // body missing
        .expect(400)
        .expect((res) => {
          expect(res.body.message).toContain('validation failed');
        });
    });

    it('should reject extra fields', () => {
      return request(app.getHttpServer())
        .post('/posts')
        .send({ 
          title: 'Test', 
          body: 'Content',
          hacker: 'field' // Campo no permitido
        })
        .expect(400);
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Testing Guards y Interceptors

// auth.guard.spec.ts
describe('AuthGuard', () => {
  let guard: AuthGuard;
  let mockExecutionContext: ExecutionContext;

  beforeEach(() => {
    guard = new AuthGuard();
    mockExecutionContext = {
      switchToHttp: () => ({
        getRequest: () => ({
          headers: { authorization: 'Bearer valid-token' }
        })
      })
    } as ExecutionContext;
  });

  it('should allow access with valid token', () => {
    const result = guard.canActivate(mockExecutionContext);
    expect(result).toBe(true);
  });

  it('should deny access without token', () => {
    mockExecutionContext.switchToHttp().getRequest = () => ({
      headers: {}
    });

    const result = guard.canActivate(mockExecutionContext);
    expect(result).toBe(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

🚀 Deployment y producción

Build optimizado

# Build para producción
npm run build

# Archivos generados en dist/
Enter fullscreen mode Exit fullscreen mode

Variables de entorno

// config/configuration.ts
export default () => ({
  port: parseInt(process.env.PORT, 10) || 3000,
  database: {
    host: process.env.DATABASE_HOST,
    port: parseInt(process.env.DATABASE_PORT, 10) || 5432,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
    expiresIn: process.env.JWT_EXPIRES_IN || '1d',
  },
});

// app.module.ts
@Module({
  imports: [
    ConfigModule.forRoot({
      load: [configuration],
      isGlobal: true,
    }),
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Docker setup

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["node", "dist/main"]
Enter fullscreen mode Exit fullscreen mode
# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:pass@db:5432/blog
    depends_on:
      - db

  db:
    image: postgres:15
    environment:
      - POSTGRES_DB=blog
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:
Enter fullscreen mode Exit fullscreen mode

Configuración para producción

// main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // CORS
  app.enableCors({
    origin: process.env.ALLOWED_ORIGINS?.split(',') || '*',
    credentials: true,
  });

  // Security headers
  app.use(helmet());

  // Rate limiting
  app.use(
    rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutos
      max: 100, // máximo 100 requests por IP
    }),
  );

  // Global validation
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    forbidNonWhitelisted: true,
    disableErrorMessages: process.env.NODE_ENV === 'production',
  }));

  // Global error handling
  app.useGlobalFilters(new AllExceptionsFilter());

  const port = process.env.PORT || 3000;
  await app.listen(port);
}
Enter fullscreen mode Exit fullscreen mode

💪 Ejercicios prácticos

🔥 Ejercicio 1: CRUD con validaciones (30 min)

Implementa un CRUD completo para Posts con:

  • Validación de título (3-120 chars)
  • Validación de contenido (mínimo 10 chars)
  • Campo status: draft, published, archived
  • Campo tags (array de strings)
// Starter code
export class CreatePostDto {
  // TODO: Añadir validaciones
  title: string;
  body: string;
  status: string;
  tags: string[];
}
Enter fullscreen mode Exit fullscreen mode
💡 Solución ```typescript export class CreatePostDto { @IsString() @Length(3, 120, { message: 'Title must be between 3 and 120 characters' }) title: string; @IsString() @MinLength(10, { message: 'Body must be at least 10 characters' }) body: string; @IsIn(['draft', 'published', 'archived']) status: string; @IsArray() @IsString({ each: true }) @ArrayMinSize(1, { message: 'At least one tag is required' }) tags: string[]; } ```

🔥 Ejercicio 2: Sistema de autenticación (45 min)

Crea un Guard que:

  1. Valide un header X-API-Key
  2. Permita ciertos endpoints sin autenticación
  3. Registre intentos de acceso no autorizado
// Starter code
@Injectable()
export class ApiKeyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // TODO: Implementar lógica
    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode
💡 Solución ```typescript @Injectable() export class ApiKeyGuard implements CanActivate { private readonly logger = new Logger(ApiKeyGuard.name); private readonly validApiKeys = process.env.VALID_API_KEYS?.split(',') || []; canActivate(context: ExecutionContext): boolean { const request = context.switchToHttp().getRequest(); const apiKey = request.headers['x-api-key']; // Endpoints públicos const publicEndpoints = ['/health', '/']; if (publicEndpoints.includes(request.url)) { return true; } if (!apiKey || !this.validApiKeys.includes(apiKey)) { this.logger.warn(`Unauthorized access attempt from ${request.ip} to ${request.url}`); return false; } return true; } } ```

🔥 Ejercicio 3: Interceptor de métricas (30 min)

Crea un Interceptor que:

  • Mida tiempo de respuesta
  • Cuente requests por endpoint
  • Añada headers personalizados
💡 Solución ```typescript @Injectable() export class MetricsInterceptor implements NestInterceptor { private static requestCounts = new Map(); intercept(context: ExecutionContext, next: CallHandler): Observable { const request = context.switchToHttp().getRequest(); const response = context.switchToHttp().getResponse(); const endpoint = `${request.method} ${request.route?.path || request.url}`; const start = Date.now(); // Incrementar contador const current = MetricsInterceptor.requestCounts.get(endpoint) || 0; MetricsInterceptor.requestCounts.set(endpoint, current + 1); return next.handle().pipe( tap(() => { const duration = Date.now() - start; response.setHeader('X-Response-Time', `${duration}ms`); response.setHeader('X-Request-Count', MetricsInterceptor.requestCounts.get(endpoint)); }) ); } } ```

🔥 Ejercicio 4: Sistema de Posts con Comentarios (60 min)

Implementa una relación uno-a-muchos:

  • Posts tienen múltiples Comments
  • Comments pertenecen a un Post
  • CRUD completo para ambos
  • Validaciones y tests

Estructura esperada:

/posts/:id/comments
GET, POST, PUT, DELETE
Enter fullscreen mode Exit fullscreen mode

🔥 Ejercicio 5: Rate Limiting personalizado (45 min)

Crea un sistema de rate limiting que:

  • Limite por IP y por API key diferente
  • Tenga diferentes límites por endpoint
  • Guarde métricas en memoria

✅ Checklist final

Básico

  • [ ] Entiendo la diferencia entre Module, Controller y Service
  • [ ] Puedo crear DTOs con validaciones complejas
  • [ ] Sé cuándo usar Guards vs Interceptors vs Middleware
  • [ ] Comprendo la inyección de dependencias

Intermedio

  • [ ] Puedo crear Guards personalizados con lógica compleja
  • [ ] Implemento Interceptors que transforman requests/responses
  • [ ] Escribo tests unitarios y de integración
  • [ ] Manejo errores globalmente

Avanzado

  • [ ] Configuré un pipeline de deployment con Docker
  • [ ] Implementé rate limiting y security headers
  • [ ] Creé un sistema de logging y métricas
  • [ ] Optimicé la aplicación para producción

🐛 Troubleshooting común

Error: "Circular dependency"


typescript
// ❌ Problema
@Injectable()
export class PostsService {
  constructor(private usersService: UsersService) {}
}

@Injectable()  
export class UsersService {
Enter fullscreen mode Exit fullscreen mode

Top comments (0)