Node.js Framework

NestJS

NestJS es un framework de Node.js opinado e inspirado en Angular. Los módulos, controladores y proveedores brindan a los equipos grandes una arquitectura compartida e inyección de dependencias integrada.

advanced16 min readUpdated 16 sept 2026
cats.controller.ts
ts
// cats.controller.ts
import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { CatsService } from "./cats.service";
import { CreateCatDto } from "./dto/create-cat.dto";

@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() createCatDto: CreateCatDto) {
    return this.catsService.create(createCatDto);
  }
}
Lanzamiento
2017
Ejecuta sobre
Express o Fastify
Lenguaje
TypeScript
Estilo
Opinado, modular
Idea central
DI y decoradores
Versión
11.x

Por que importa

Por qué los equipos eligen NestJS

Estructura para equipos grandes

Los módulos, capas y decoradores proporcionan el mismo mapa para todos. Los nuevos desarrolladores pueden encontrar dónde reside una funcionalidad sin leer todo el código base.

Inyección de dependencias integrada

Un contenedor real de inversión de control resuelve los constructores, gestiona los ciclos de vida y hace que cada dependencia sea intercambiable en las pruebas.

Preocupaciones transversales como código

Los guards, interceptores, pipes y filtros gestionan la autenticación, el logging, la validación y los errores de forma declarativa, en la capa adecuada.

La imagen completa

Módulos, proveedores, controladores

Un módulo define el límite de una funcionalidad, un proveedor contiene la lógica y un controlador convierte las peticiones HTTP en llamadas a métodos.

Módulos

Límite

Cada funcionalidad es un módulo que declara qué importa, provee y exporta, haciendo que las dependencias sean explícitas.

Proveedores

Inyectar

Los servicios, repositorios y fábricas son proveedores que el contenedor instancia e inyecta a través del constructor.

Controladores

Ruta

Clases decoradas que mapean métodos y rutas HTTP a métodos manejadores, con parámetros extraídos mediante decoradores.

HTML5 de un vistazo

La caja de herramientas de NestJS

Módulos

@Module agrupa controladores y proveedores en el límite de una funcionalidad.

Controladores

@Controller y @Get convierten una clase en un conjunto de rutas.

Proveedores

Las clases @Injectable son resueltas y compartidas por el contenedor de DI.

DTOs

Formas de petición tipadas y validadas por pipes antes de que se ejecute el manejador.

Guards

Deciden si una petición puede proceder, utilizando roles o políticas.

Interceptores

Envuelven la ejecución del manejador para logging, caching y mapeo de respuestas.

La guia completa

NestJS: Todo lo que necesitas saber

¿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 ValidationPipe global.
  • 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.env directamente.
  • Prefiere el scope de singleton; recurre al request scope solo cuando sea estrictamente necesario.
  • Realiza pruebas con TestingModule y sobrescribe las dependencias en lugar de hacer peticiones a servicios reales.

Errores comunes

  • Construir providers con new en 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 export el 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.

En la practica

Los cuatro archivos de una funcionalidad

Un controlador enruta, un servicio contiene la lógica, un módulo los conecta y un DTO define y valida la entrada.

cats.controller.ts
import {
  Body, Controller, Delete, Get, Param, Post,
} from "@nestjs/common";
import { CatsService } from "./cats.service";
import { CreateCatDto } from "./dto/create-cat.dto";

@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);
  }

  @Delete(":id")
  remove(@Param("id") id: string) {
    return this.catsService.remove(id);
  }
}

Obtención de una dependencia

La inyección por constructor permite que el contenedor cree y comparta el servicio, y permite que las pruebas sustituyan uno falso. Instanciarlo manualmente anula el propósito del framework.

Preferido
@Controller("cats")
export class CatsController {
  constructor(private readonly catsService: CatsService) {}
}
Evitar
@Controller("cats")
export class CatsController {
  // Bypasses DI, breaks the lifecycle and
  // cannot be replaced in a test.
  private catsService = new CatsService();
}

Validación de entrada

Un DTO con decoradores de class-validator más un ValidationPipe global rechaza peticiones incorrectas antes del manejador y produce respuestas 400 consistentes.

Preferido
export class CreateCatDto {
  @IsString()
  @MinLength(1)
  name: string;

  @IsInt()
  age: number;
}

@Post()
create(@Body() dto: CreateCatDto) {
  return this.catsService.create(dto);
}
Evitar
@Post()
create(@Body() body: any) {
  if (typeof body.name !== "string") {
    throw new BadRequestException();
  }
  // every handler repeats the same checks
}

Compromisos

¿Es NestJS la opción predeterminada correcta?

NestJS sacrifica libertad en favor de la estructura. Las convenciones dan frutos a medida que el código base crece, pero pueden sentirse pesadas cuando no es así.

Strengths

  • Estructura desde el primer día

    Los módulos, controladores y proveedores dan un lugar a cada funcionalidad, por lo que un equipo en crecimiento comparte una arquitectura en lugar de inventar una.

  • Inyección de dependencias integrada

    Un contenedor real gestiona los ciclos de vida y hace que cada dependencia sea intercambiable, manteniendo los servicios testeables sin estado global.

  • Baterías incluidas

    La validación, guards, interceptores, configuración y utilidades de testing vienen con el framework, por lo que ensamblas mucho menos por tu cuenta.

Trade-offs

  • Mucha ceremonia

    Los decoradores, módulos y DTOs añaden archivos y conceptos antes de que el primer endpoint funcione, por lo que los servicios pequeños pueden sentirse sobre-ingenierizados.

  • Adoptas sus opiniones

    NestJS decide cómo se organiza el código. Luchar contra esas convenciones cuesta más que elegir un framework más ligero.

  • Se asume el uso de TypeScript

    Los decoradores y la inyección de dependencias dependen de tipos y metadatos, por lo que un equipo de JavaScript puro se enfrenta a una curva de aprendizaje real.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender NestJS?

Nuestro tutorial interactivo te guia a traves de NestJS paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.