Saltar al contenido principal
60 min de lectura1 de agosto de 2026
NestJSTypeScriptBackendAPIDecoradoresNode

Requisitos previos

  • typescript
  • node-express

Lo que aprenderás

  • Estructurar aplicaciones con módulos de NestJS
  • Crear controllers y services con decoradores
  • Validar entradas con DTOs y ValidationPipe
  • Persistir datos con TypeORM
  • Proteger rutas con guards y JWT
Tutorial60 min

NestJS: APIs estructuradas con TypeScript

NestJS es el framework para construir APIs backend en Node.js con TypeScript de forma estructurada y escalable. Basado en decoradores y en una arquitectura por módulos, te da organización, validación, autenticación y documentación desde el primer día. Requiere conocer TypeScript y algo de Node/Express.


Qué es NestJS

NestJS es un framework backend progresivo para Node.js, escrito íntegramente en TypeScript. Nació en 2017 y se inspira en la arquitectura de Angular: decoradores para definir rutas, inyección de dependencias integrada y una separación clara en módulos, controllers y services.

Sobre Express (o Fastify, según configuración) añade una capa de estructura que Express no impone: las rutas se agrupan por módulos, cada módulo declara qué controllers expone y qué services provee, y el framework se encarga de conectar todo. El resultado es que un proyecto grande no se convierte en un caos de middlewares sueltos.

Comparación rápida: Express te deja organizar el código como quieras (libertad total, riesgo total). NestJS impone una arquitectura clara: cada feature vive en un módulo con su controller (HTTP), su service (lógica de negocio) y sus DTOs (validación de datos). El coste es algo más de código inicial; el beneficio, consistencia a escala.

El ecosistema de NestJS es completo: TypeORM o Prisma para bases de datos, Passport/JWT para autenticación, Swagger para documentación, colas, WebSockets, GraphQL y testing unitario/e2e incluido. Todo con el mismo patrón de decoradores.

CLI y crear proyecto

La CLI de NestJS es imprescindible: genera proyectos y piezas (módulos, controllers, services) con un comando:

# Instalar la CLI globalmente
npm i -g @nestjs/cli

# Crear un proyecto nuevo
nest new mi-api

# Entrar y arrancar en modo desarrollo
cd mi-api
npm run start:dev

nest new pregunta el gestor de paquetes (npm, yarn o pnpm) y genera la estructura estándar:

mi-api/
├── src/
│   ├── app.controller.ts     # controller raíz
│   ├── app.controller.spec.ts# tests unitarios
│   ├── app.module.ts         # módulo raíz
│   ├── app.service.ts        # service raíz
│   └── main.ts               # punto de entrada
├── test/
│   ├── app.e2e-spec.ts
│   └── jest-e2e.json
├── nest-cli.json
├── package.json
└── tsconfig.json

El punto de entrada main.ts crea la aplicación a partir del módulo raíz:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

Con npm run start:dev el servidor se recarga en caliente ante cada cambio. Comprueba que responde en http://localhost:3000.

Genera piezas con la CLI en vez de copiar código a mano: nest g module tareas, nest g controller tareas y nest g service tareas. Crean los archivos y los registran en el módulo automáticamente.

Módulos

Un módulo es la unidad de organización. Se declara con el decorador @Module() y recibe: controllers (rutas que expone), providers (services inyectables) e imports (otros módulos que necesita):

import { Module } from '@nestjs/common';
import { TareasController } from './tareas.controller';
import { TareasService } from './tareas.service';

@Module({
  controllers: [TareasController],
  providers: [TareasService],
})
export class TareasModule {}

Cada feature de la aplicación vive en su propio módulo. Para que un service de un módulo sea visible en otro, hay que exports:

import { Module } from '@nestjs/common';
import { NotificacionesService } from './notificaciones.service';

@Module({
  providers: [NotificacionesService],
  exports: [NotificacionesService], // visible para otros módulos
})
export class NotificacionesModule {}

Y el módulo raíz AppModule importa todos los demás:

import { Module } from '@nestjs/common';
import { TareasModule } from './tareas/tareas.module';
import { NotificacionesModule } from './notificaciones/notificaciones.module';

@Module({
  imports: [TareasModule, NotificacionesModule],
})
export class AppModule {}

Controllers

Un controller define las rutas HTTP de un módulo. Se declara con @Controller('ruta') y cada método expone un endpoint con un decorador de método: @Get, @Post, @Patch, @Delete:

import { Body, Controller, Get, Param, Post, Query } from '@nestjs/common';
import { TareasService } from './tareas.service';
import { CrearTareaDto } from './dto/crear-tarea.dto';

@Controller('tareas')
export class TareasController {
  constructor(private readonly tareasService: TareasService) {}

  // GET /tareas?done=true
  @Get()
  listar(@Query('done') done?: string) {
    return this.tareasService.listar();
  }

  // GET /tareas/42
  @Get(':id')
  obtener(@Param('id') id: string) {
    return this.tareasService.obtener(id);
  }

  // POST /tareas  con body JSON
  @Post()
  crear(@Body() dto: CrearTareaDto) {
    return this.tareasService.crear(dto);
  }
}
Los decoradores de parámetros extraen datos de la petición: @Param('id') lee un parámetro de ruta, @Query() los query params y @Body() el JSON del cuerpo. En main.ts puedes añadir app.enableCors() si el frontend vive en otro dominio.

Los códigos de estado son automáticos: 200/201 para respuestas exitosas, y las excepciones lanzadas desde el service producen errores HTTP correctos. Para respuestas con código específico usa @HttpCode(201).

Services e inyección de dependencias

Un service contiene la lógica de negocio. Se marca con @Injectable() y se inyecta en el controller mediante el constructor. NestJS resuelve las dependencias automáticamente: no instancias services a mano.

import { Injectable, NotFoundException } from '@nestjs/common';
import { CrearTareaDto } from './dto/crear-tarea.dto';

export interface Tarea {
  id: string;
  titulo: string;
  done: boolean;
}

@Injectable()
export class TareasService {
  private tareas: Tarea[] = [];

  listar(): Tarea[] {
    return this.tareas;
  }

  obtener(id: string): Tarea {
    const tarea = this.tareas.find((t) => t.id === id);
    if (!tarea) {
      throw new NotFoundException('Tarea no encontrada');
    }
    return tarea;
  }

  crear(dto: CrearTareaDto): Tarea {
    const tarea: Tarea = {
      id: String(this.tareas.length + 1),
      titulo: dto.titulo,
      done: false,
    };
    this.tareas.push(tarea);
    return tarea;
  }
}
Lanzar excepciones de NestJS (NotFoundException, BadRequestException, UnauthorizedException...) en el service convierte la lógica de negocio en respuestas HTTP correctas sin acoplar el service a Express.

Los services son singletons por módulo: todos los que lo inyectan comparten la misma instancia. Si necesitas un ciclo de vida por petición, usa el decorador @Injectable{ scope: Scope.REQUEST }.

DTOs y validación

Un DTO (Data Transfer Object) define la forma de los datos de entrada. Con class-validator y class-transformer, los decoradores de los campos validan el body antes de que llegue a tu lógica:

# Instalar las dependencias de validación
npm i class-validator class-transformer

Define el DTO con decoradores de validación:

import { IsBoolean, IsNotEmpty, IsOptional, IsString, MinLength } from 'class-validator';

export class CrearTareaDto {
  @IsString()
  @IsNotEmpty()
  @MinLength(3, { message: 'El título debe tener al menos 3 caracteres' })
  titulo: string;

  @IsOptional()
  @IsBoolean()
  done?: boolean;
}

Activa el ValidationPipe globalmente en main.ts. La opción whitelist: true elimina campos no declarados en el DTO:

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,        // descarta campos desconocidos
      forbidNonWhitelisted: true, // o lanza 400 si llegan
      transform: true,        // convierte los tipos automáticamente
    }),
  );
  await app.listen(3000);
}
bootstrap();
Sin el pipe global, los DTOs son solo tipado en tiempo de compilación: si el cliente envía JSON inválido, tu service lo recibe igualmente. El ValidationPipe es lo que convierte las reglas del DTO en respuestas 400 reales. Es obligatorio en producción.

Con transform: true, los query params (que siempre llegan como string) se convierten al tipo declarado: @Query('page') page: number recibe un número real, no un string.

TypeORM + SQLite/PostgreSQL

Para persistir datos, NestJS se integra con TypeORM a través del módulo @nestjs/typeorm. Empezamos con SQLite (cero configuración, perfecto para desarrollo) y el cambio a PostgreSQL es de una línea:

# SQLite (desarrollo local)
npm i @nestjs/typeorm typeorm better-sqlite3

# Para PostgreSQL en producción
npm i pg

Configura la conexión en el AppModule:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TareasModule } from './tareas/tareas.module';
import { Tarea } from './tareas/tarea.entity';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'better-sqlite3',
      database: 'db.sqlite',
      entities: [Tarea],
      synchronize: true, // crea las tablas automáticamente (solo dev)
    }),
    TareasModule,
  ],
})
export class AppModule {}

Define la entidad que mapea a una tabla:

import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

@Entity('tareas')
export class Tarea {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  titulo: string;

  @Column({ default: false })
  done: boolean;
}

Inyecta el repositorio en el service con @InjectRepository y regístralo en el módulo con TypeOrmModule.forFeature([Tarea]):

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Tarea } from './tarea.entity';
import { CrearTareaDto } from './dto/crear-tarea.dto';

@Injectable()
export class TareasService {
  constructor(
    @InjectRepository(Tarea)
    private readonly repo: Repository<Tarea>,
  ) {}

  listar(): Promise<Tarea[]> {
    return this.repo.find();
  }

  crear(dto: CrearTareaDto): Promise<Tarea> {
    const tarea = this.repo.create(dto);
    return this.repo.save(tarea);
  }
}
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Tarea } from './tarea.entity';
import { TareasController } from './tareas.controller';
import { TareasService } from './tareas.service';

@Module({
  imports: [TypeOrmModule.forFeature([Tarea])],
  controllers: [TareasController],
  providers: [TareasService],
})
export class TareasModule {}
El patrón repository separa la base de datos de la lógica de negocio: el service trabaja con el repositorio y no escribe SQL a mano. Para consultas complejas, el repositorio ofrece findOne, findBy, queryBuilder y paginación con take/skip.

Para PostgreSQL en producción, solo cambia la configuración deforRoot y añade las variables de entorno:

TypeOrmModule.forRoot({
  type: 'postgres',
  host: process.env.DB_HOST,
  port: 5432,
  username: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME,
  entities: [Tarea],
  synchronize: false, // en producción usa migraciones
}),

Guards y JWT

Los guards deciden si una petición puede continuar. Se usan para autenticación y autorización. Para JWT, instala los paquetes y configura el módulo:

npm i @nestjs/jwt
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';

@Module({
  imports: [
    JwtModule.register({
      global: true,
      secret: process.env.JWT_SECRET || 'cambia-esto-en-produccion',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService],
})
export class AuthModule {}

Un guard personalizado verifica el token del header Authorization: Bearer <token>:

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwtService: JwtService) {}

  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const authHeader: string | undefined = request.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new UnauthorizedException('Token no proporcionado');
    }

    const token = authHeader.slice(7);
    try {
      request.user = this.jwtService.verify(token);
      return true;
    } catch {
      throw new UnauthorizedException('Token inválido o expirado');
    }
  }
}

Aplica el guard a rutas o controllers enteros con @UseGuards():

import { Controller, Get, UseGuards } from '@nestjs/common';
import { JwtAuthGuard } from './jwt-auth.guard';

@Controller('perfil')
@UseGuards(JwtAuthGuard) // protege todas las rutas del controller
export class PerfilController {
  @Get()
  datos() {
    return { mensaje: 'Acceso autorizado' };
  }
}
El guard solo protege las rutas donde se aplica: una ruta sin @UseGuards queda abierta. Por defecto, todo debería estar protegido y solo los endpoints públicos (login, registro, health) deberían permitir el acceso sin token.

Swagger

Swagger documenta tu API automáticamente y te da una interfaz interactiva en /docs donde probar cada endpoint. Instala el paquete y configura el documento:

npm i @nestjs/swagger
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

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

  const config = new DocumentBuilder()
    .setTitle('Mi API')
    .setDescription('API de tareas construida con NestJS')
    .setVersion('1.0')
    .addBearerAuth() // botón de autorización para JWT en la UI
    .build();

  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup('docs', app, document);

  await app.listen(3000);
}
bootstrap();

Enriquece controllers y endpoints con decoradores de descripción:

import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get } from '@nestjs/common';

@ApiTags('tareas') // agrupa en la UI de Swagger
@Controller('tareas')
export class TareasController {
  @ApiOperation({ summary: 'Listar todas las tareas' })
  @Get()
  listar() {
    return this.tareasService.listar();
  }
}
Swagger usa el tipo de retorno de cada método y los DTOs para generar la documentación de forma automática. Con @ApiBearerAuth() en el controller y addBearerAuth() en la configuración, la UI muestra el botón Authorize para probar los endpoints protegidos.

Ejercicios


NestJS convierte un proyecto Express típico en una arquitectura clara y escalable: módulos que agrupan controllers y services, validación con DTOs, persistencia con TypeORM y seguridad con guards. Con Swagger, tu API queda documentada sin esfuerzo extra. Los siguientes pasos son las migraciones con TypeORM, Passport para estrategias de login (local, OAuth), testing con @nestjs/testing y WebSockets o GraphQL si tu aplicación lo necesita. Es la opción natural si quieres un backend TypeScript serio y mantenible en el tiempo.