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.
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:devnest 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.jsonEl 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.
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);
}
}@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;
}
}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-transformerDefine 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();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 pgConfigura 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 {}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/jwtimport { 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' };
}
}@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/swaggerimport { 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();
}
}@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.