¿Qué es NestJS?
NestJS es un framework progresivo de Node.js que aporta estructura al JavaScript del lado del servidor. Mientras que Express te ofrece primitivas y te deja elegir, NestJS te proporciona una arquitectura: módulos, controladores, proveedores, decoradores y un contenedor de inyección de dependencias. Se inspira fuertemente en Angular y en los frameworks empresariales de Java, y se ejecuta sobre Express o Fastify.
Esa postura opinada es precisamente su propuesta de valor. En un equipo de cinco personas que construye una API grande, los problemas más difíciles rara vez son de HTTP. Son el nombrado, los límites, las pruebas y el mantenimiento de patrones consistentes a medida que la base de código crece. NestJS resuelve esto con convenciones que puedes aceptar o reemplazar, y ya incluye toda la infraestructura —validación, guards, interceptores, configuración, utilidades de prueba— para que no tengas que ensamblarla tú mismo.
Los módulos definen los límites
Todo en NestJS está organizado en módulos. Un módulo es una clase decorada con @Module que declara sus controladores, sus proveedores y lo que exporta al resto de la aplicación.
@Module({
imports: [DatabaseModule],
controllers: [CatsController],
providers: [CatsService],
exports: [CatsService],
})
export class CatsModule {}
imports importa otros módulos cuyos proveedores exportados necesites. providers son las clases que pertenecen a este módulo. exports es la API pública: solo lo que un módulo exporta puede ser inyectado en otro lugar. Esto hace que las dependencias sean explícitas; si OrdersModule necesita CatsService, debe importar CatsModule, y el contenedor (que actúa como un compilador) te avisará cuando no pueda resolver algo.
El AppModule raíz importa cada módulo de funcionalidad. Los módulos de funcionalidad pueden agruparse en módulos compartidos o core, y @Global() marca un módulo que debe estar disponible en todas partes sin necesidad de importaciones repetidas. Usa los módulos globales con moderación para infraestructura genuinamente transversal, como la configuración o el logging.
Controladores: el enrutamiento mediante decoradores
Un controlador mapea las solicitudes HTTP a métodos. El decorador de clase define la ruta base, mientras que los decoradores de método definen el verbo y la subruta.
@Controller("cats")
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(":id")
findOne(@Param("id") id: string) {
return this.catsService.findOne(id);
}
@Post()
create(@Body() dto: CreateCatDto) {
return this.catsService.create(dto);
}
}
Los decoradores de parámetros se encargan de la extracción: @Param, @Query, @Body, @Headers, @Req y @Res. El manejador devuelve un valor simple o una promesa, y NestJS lo serializa a JSON con el estado correspondiente. Si lanzas una HttpException como NotFoundException, el framework la convierte en una respuesta de error adecuada.
Los controladores deben ser ligeros. Su función es traducir la solicitud HTTP en una llamada a un método y nada más. Toda la lógica debe residir en los providers, lo que permite que el controlador sea testeable y que las preocupaciones de HTTP permanezcan aisladas.
Providers e inyección de dependencias
Un provider es cualquier clase que pueda ser inyectada. Márcala con @Injectable() y declárala en un módulo, y el contenedor se encargará del resto.
@Injectable()
export class CatsService {
constructor(private readonly db: DatabaseService) {}
findAll() {
return this.db.query("SELECT * FROM cats");
}
}
La inyección se realiza a través del constructor y por tipo. El contenedor lee los metadatos emitidos, resuelve cada parámetro y pasa la instancia. También puedes inyectar mediante un token cuando el valor no es una clase, utilizando @Inject("CONFIG") con un provider coincidente.
Los providers pueden personalizarse con factories, valores y alias. Un factory provider es la forma habitual de inyectar algo que requiere una configuración asíncrona, como una conexión a una base de datos:
@Module({
providers: [
{
provide: "DATABASE",
useFactory: async (config: ConfigService) => {
return createPool(config.getOrThrow("DATABASE_URL"));
},
inject: [ConfigService],
},
],
exports: ["DATABASE"],
})
export class DatabaseModule {}
El scope también es importante. El scope singleton por defecto comparte una única instancia en toda la aplicación, que es lo que se busca para servicios sin estado. El scope REQUEST crea una nueva instancia por cada solicitud y es útil para contextos específicos de la petición, pero afecta al rendimiento ya que se propaga hacia arriba en el grafo de dependencias.
DTOs, pipes y validación
La validación de entradas es una prioridad fundamental. Un DTO (data transfer object) es una clase que describe la estructura de una solicitud, decorada con reglas de class-validator.
export class CreateCatDto {
@IsString()
@MinLength(1)
name: string;
@IsInt()
@Min(0)
age: number;
}
Un pipe transforma o valida el valor antes de que llegue al handler. Activa el ValidationPipe integrado de forma global y cada DTO decorado se validará automáticamente:
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
);
whitelist elimina las propiedades desconocidas, forbidNonWhitelisted las rechaza directamente y transform convierte los payloads en instancias reales de DTO para que los tipos sean reales en tiempo de ejecución. Una entrada inválida genera una 400 estructurada sin necesidad de escribir una sola línea de código de validación en el handler. Los pipes también pueden aplicarse por parámetro o por controlador mediante @UsePipes.
Guards, interceptors y filters
Estos tres bloques fundamentales cubren la mayoría de las preocupaciones transversales (cross-cutting concerns) y se ejecutan en un orden fijo.
Los Guards se ejecutan antes del handler y deciden si la solicitud debe continuar.
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>("roles", context.getHandler());
if (!roles) return true;
const { user } = context.switchToHttp().getRequest();
return roles.includes(user?.role);
}
}
Los Interceptors envuelven la llamada al handler y pueden transformar el resultado, medir el tiempo o añadir almacenamiento en caché.
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const start = Date.now();
return next.handle().pipe(
tap(() => console.log(`took ${Date.now() - start}ms`)),
);
}
}
Los Exception filters capturan los errores lanzados y dan formato a la respuesta. Sin uno, NestJS utiliza un valor predeterminado razonable; un filtro personalizado mantiene la estructura de los errores consistente en toda la API.
@Catch(HttpException)
export class HttpErrorFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const response = host.switchToHttp().getResponse();
const status = exception.getStatus();
response.status(status).json({
statusCode: status,
message: exception.message,
timestamp: new Date().toISOString(),
});
}
}
Regístralos de forma global, por controlador o por ruta. Los guards, interceptors y filters globales son la manera en que una aplicación de NestJS madura centraliza la autenticación, la observabilidad y el manejo de errores.
Configuración y entorno
El paquete @nestjs/config envuelve las variables de entorno y permite que sean inyectables. Impórtalo una sola vez como un módulo global.
@Module({
imports: [ConfigModule.forRoot({ isGlobal: true })],
})
export class AppModule {}
Luego, inyecta ConfigService donde sea necesario:
@Injectable()
export class DatabaseService {
constructor(private readonly config: ConfigService) {}
connect() {
const url = this.config.getOrThrow<string>("DATABASE_URL");
return createPool(url);
}
}
Se recomienda usar getOrThrow para los valores obligatorios, de modo que si falta una variable, el sistema falle al iniciar en lugar de hacerlo en la primera solicitud. Combina esto con un esquema de validación para que la configuración se verifique una sola vez y mantén los secretos totalmente fuera del repositorio.
Pruebas con TestingModule
Las utilidades de prueba de NestJS crean un contenedor real de forma aislada. Declaras únicamente los providers que deseas probar y luego sobrescribes sus dependencias con mocks.
describe("CatsController", () => {
let controller: CatsController;
let service: CatsService;
beforeEach(async () => {
const module = await Test.createTestingModule({
controllers: [CatsController],
providers: [CatsService],
}).compile();
controller = module.get(CatsController);
service = module.get(CatsService);
});
it("returns all cats", () => {
jest.spyOn(service, "findAll").mockReturnValue([]);
expect(controller.findAll()).toEqual([]);
});
});
Para las pruebas end-to-end, construye la aplicación completa y utiliza supertest contra app.getHttpServer(). Debido a que el contenedor de DI es el mismo que utiliza la producción, estas pruebas ejercitan los guards, pipes y filters exactamente como están desplegados.
Mejores prácticas
- Mantén los controladores ligeros y coloca toda la lógica en servicios inyectables.
- Define los límites de los módulos basándote en funcionalidades y exporta solo lo que otros módulos necesiten.
- Valida cada solicitud con un DTO y un
ValidationPipeglobal. - Utiliza guards para la autenticación y autorización, e interceptores para dar formato a las respuestas.
- Registra filtros de excepciones globales para que las respuestas de error tengan una estructura uniforme.
- Inyecta la configuración en lugar de leer
process.envdirectamente. - Prefiere el scope de singleton; recurre al request scope solo cuando sea estrictamente necesario.
- Realiza pruebas con
TestingModuley sobrescribe las dependencias en lugar de hacer peticiones a servicios reales.
Errores comunes
- Construir providers con
newen lugar de inyectarlos, lo que rompe el contenedor. - Olvidar añadir un provider a un módulo y terminar persiguiendo un error de “Nest can’t resolve dependencies”.
- Importar un módulo pero olvidar
exportel provider que debería compartir. - Colocar la lógica de negocio en los controllers, haciendo que sean imposibles de testear mediante unit tests.
- Usar request scope en todas partes, degradando el rendimiento silenciosamente.
- Confiar en
@Body()sin un DTO y un validation pipe. - Dispersar el formateo de errores en lugar de centralizarlo en un filter.
- Tratar los módulos como simples carpetas en lugar de límites de dependencias.
Próximos pasos
NestJS es la forma más estructurada de construir un backend en Node.js y se apoya directamente sobre Express o Fastify. Lee esas guías para comprender la capa HTTP que estás abstrayendo. Un dominio sólido de TypeScript es esencial, ya que los decoradores y la inyección de dependencias se basan en los tipos, y los conceptos básicos de Node.js explican el runtime sobre el cual se ejecuta todo. Después, construye un módulo de funcionalidad de principio a fin: controlador, servicio, DTO y prueba.