🚨 ¡Nueva review! ✨ Mi ratón favorito para programar: el Logitech MX Master 3S . ¡Échale un ojo! 👀

¿Qué es NestJS y por qué lo necesitas? Setup con Docker desde cero

Serie NestJS #1 — Tu primera API tipada corriendo en contenedores

Escrito por domin el 20 de marzo de 2026

Primer post de una serie larga. Pero larga larga! Porque vamos a aprender NestJS desde cero absoluto hasta montar una arquitectura enterprise con microservicios, CQRS, colas, WebSockets y todo lo que nos podamos imaginar. Y todo con Docker, todo tipado con TypeScript a fuego y siguiendo la documentación oficial al dedillo.

¿Por qué NestJS? ejejejeje Pues porque me hace falta machacarlo para tenerlo bien fresco y porque aparentemente se está pidiendo mucho en ofertas de trabajo, así que vamos a echarle un ojito a ver si nos gusta.

EA, vamos al lío.

Logo de NestJS sobre fondo rojo oscuro con un contenedor Docker al lado.

1. Express está muy bien, pero…

Si vienes de Express, sabes que es una maravilla para montar algo rápido, cuatro líneas mal tirás y tienes un servidor corriendo. El problema viene cuando el proyecto crece, y crece un montón!

Con Express a pelo no tienes ninguna estructura definida y cada desarrollador organiza las carpetas como le apetece. Uno mete los controllers en /routes, otro en /api, otro mezcla todo en un index.js de 3000 líneas. No hay ninguna estructura o patrón definido de forma oficial, cada uno se lo guisa como quiere, y si te toca mantener un proyecto de Express vas a tener que dedicarle un tiempo a ver como se está organizando y donde está cada cosa.

⚡ Express puro

Libertad total de estructura. Genial para prototipos y APIs pequeñas, pero en proyectos grandes cada repo es un mundo. No hay patrón, no hay inyección de dependencias, no hay módulos. Cada equipo reinventa la rueda.

🏗️ NestJS

Arquitectura modular con criterio. Controllers, Services, Modules, inyección de dependencias, decoradores, pipes, guards... Todo organizado y con un patrón claro que cualquier developer de NestJS reconoce al instante.

Y ojo que no estoy diciendo que Express sea malo, para nada! Express es la base de NestJS (literalmente corre por debajo). Lo que quiero decir es que NestJS te da estructura, convenciones y herramientas que con Express tendrías que montar tú a mano.


2. ¿Qué es NestJS exactamente?

Vale pavo, ¿pero qué es NestJS? es un framework para construir aplicaciones backend con Node.js. Está escrito en TypeScript, usa decoradores para casi todo y tiene una arquitectura modular que te conduce a organizar el código de una manera limpia y mantenible.

Algunos detalles importantes sobre NestJS:

Un ojito rápido a la arquitectura

NestJS organiza todo en tres conceptos fundamentales:

🎯 Controllers

Reciben las peticiones HTTP y devuelven respuestas. Son los que definen las rutas de tu API y no deberían tener lógica de negocio.

⚙️ Providers (Services)

Donde vive la lógica de negocio. Se inyectan en los controllers o en otros services mediante inyección de dependencias.

📦 Modules

Agrupan controllers y providers relacionados. Cada feature de tu app es un módulo. El AppModule es el raíz que lo une todo.

En los próximos posts de la serie vamos a profundizar en cada uno de estos conceptos. Hoy nos centramos en montar el entorno y que todo funcione.


3. Requisitos previos

Para seguir esta serie necesitas:


4. Montando el entorno con Docker

No vas a instalar Node.js en tu máquina. Nada de ensuciar tu sistema con diferentes versiones globales. Todo va a ir en contenedores. Así cualquiera que clone el repo tiene exactamente el mismo entorno, da igual si usa Windows, Mac o Linux.

4.1. Crear el proyecto

Primero creamos la carpeta del proyecto y generamos el proyecto de NestJS usando el CLI directamente desde Docker, sin instalar nada en local:

Crear proyecto NestJS 0 / 2
~$
Pulsa para ejecutar el siguiente comando

¿Qué acabamos de hacer? Usar un contenedor temporal de Node 22 Alpine para ejecutar el CLI de NestJS y generar el proyecto. El flag --strict activa las opciones más estrictas de TypeScript, que es lo que queremos, tipado fuerte a fuego. El --rm borra el contenedor cuando termina, así no deja basura.

Ojito aquí: como el contenedor de Docker corre como root, los archivos generados pertenecen al usuario root de tu máquina y no vas a poder editarlos. Para arreglar esto, cambiamos el propietario de la carpeta a tu usuario:

sudo chown -R $USER:$USER api/

E YA. Ahora ya puedes crear y editar archivos dentro de api/ sin problemas. Esto solo hay que hacerlo una vez, cuando generas el proyecto.

4.2. El Dockerfile

Dentro de la carpeta api/ que nos ha generado el CLI, creamos el Dockerfile para desarrollo:

# Dockerfile
FROM node:22-alpine

WORKDIR /app

# Copiamos los archivos de dependencias primero (para aprovechar la caché de Docker)
COPY package*.json ./

RUN npm ci

# Copiamos el resto del código
COPY . .

# Puerto que expone NestJS
EXPOSE 3000

# Arrancamos en modo watch (hot reload)
CMD ["npm", "run", "start:dev"]

Aquí usamos npm ci en vez de npm install porque ci instala exactamente las versiones del package-lock.json, sin sorpresas. Y arrancamos con start:dev que activa el hot reload para que cada vez que guardes un archivo, el servidor se reinicie solo.

4.3. Docker Compose

En la raíz del proyecto (nestjs-course/), creamos el docker-compose.yml:

services:
    api:
        build:
            context: ./api
            dockerfile: Dockerfile
        ports:
            - '3000:3000'
        volumes:
            - ./api:/app
            - /app/node_modules
        environment:
            - NODE_ENV=development

Para trabajar bien con Docker, la cosa está en los volumes. Montamos ./api:/app para que los cambios que hagas en tu editor se reflejen dentro del contenedor al momento. Pero usamos /app/node_modules como volumen anónimo para que los node_modules del contenedor no se machaquen con los de tu máquina local si los tuvieras. Es un patrón clásico de Docker para desarrollo con Node.js.

4.4. Arrancamos

Arrancar NestJS con Docker 0 / 2
~$
Pulsa para ejecutar el siguiente comando

EA, ya tienes NestJS corriendo en Docker y sin instalar Node en tu máquina. Si abres http://localhost:3000 en el navegador, verás el Hello World!.


5. Entendiendo la estructura del proyecto

Vamos a cotillear que nos ha generado el CLI y con qué estructura que aquí no hay nada hecho a lo loco.

api/
├── src/
   ├── app.controller.ts        # Controller: recibe peticiones HTTP
   ├── app.controller.spec.ts   # Tests del controller
   ├── app.module.ts            # Módulo raíz: une todo
   ├── app.service.ts           # Service: lógica de negocio
   └── main.ts                  # Punto de entrada de la aplicación
├── test/
   ├── app.e2e-spec.ts          # Tests end-to-end
   └── jest-e2e.json            # Config de Jest para E2E
├── Dockerfile
├── nest-cli.json                # Configuración del CLI de Nest
├── package.json
├── tsconfig.json                # Config de TypeScript
└── tsconfig.build.json          # Config de TypeScript para build

Solo 5 archivos en src/. Minimalista y aesthetic o como se diga, todo muy claro. Vamos a ver qué hace cada uno.


6. De main.ts al Hello World

6.1. main.ts — El punto de entrada

Todo empieza aquí, este es el archivo que arranca la aplicación:

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

async function bootstrap(): Promise<void> {
    const app = await NestFactory.create(AppModule);
    await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

NestFactory.create() recibe el módulo raíz de la aplicación y monta todo el árbol de dependencias, es como el arranque del motor. Llamas a bootstrap(), NestJS escanea todos los módulos, resuelve las dependencias, registra los controllers y levanta el servidor en el puerto 3000 o el que tengas en la variable de entorno PORT.

6.2. app.module.ts — El módulo raíz

// src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
    imports: [],
    controllers: [AppController],
    providers: [AppService],
})
export class AppModule {}

El decorador @Module() le dice a NestJS:

Esto es el centro de la de NestJS. Cada funcionalidad que vayas añadiendo será un módulo nuevo que se importa aquí.

6.3. app.controller.ts — El controller

// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get()
    getHello(): string {
        return this.appService.getHello();
    }
}

Varias cosas importantes aquí:

6.4. app.service.ts — El service

// src/app.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class AppService {
    getHello(): string {
        return 'Hello World!';
    }
}

El decorador @Injectable() marca la clase como un provider que NestJS puede inyectar donde haga falta. La lógica de negocio vive aquí, no en el controller. El controller solo recibe la petición, llama al service y devuelve la respuesta, así conseguimos una buena separacion de responsabilidades.

El flujo completo

Cuando haces GET http://localhost:3000, esto es lo que pasa por debajo:

📨 Paso 1

La petición HTTP llega al servidor Express que corre dentro de NestJS.

🔍 Paso 2

NestJS busca qué controller y qué método responde a GET /. Encuentra AppController.getHello().

⚙️ Paso 3

El controller llama a this.appService.getHello(), que devuelve "Hello World!".

✅ Paso 4

NestJS serializa la respuesta y la envía al cliente con status 200.


7. Tu primer endpoint tipado propio

El Hello World está muy bien pero vamos a hacer algo un poco más real, como añadir un endpoint que devuelva información de la API. Editamos los archivos que ya tenemos:

// src/app.service.ts
import { Injectable } from '@nestjs/common';

export interface ApiInfo {
    name: string;
    version: string;
    description: string;
    status: 'running' | 'maintenance';
}

@Injectable()
export class AppService {
    getHello(): string {
        return 'Hello World!';
    }

    getApiInfo(): ApiInfo {
        return {
            name: 'NestJS Course API',
            version: '1.0.0',
            description: 'API del curso de NestJS desde cero hasta enterprise',
            status: 'running',
        };
    }
}

Fíjate que hemos definido una interfaz ApiInfo exportada con un tipo literal para status que solo puede ser 'running' o 'maintenance'. Si intentas devolver otro valor, TypeScript te pega. La exportamos porque el controller necesita poder referenciar ese tipo.

Ahora añadimos la ruta en el controller:

// src/app.controller.ts
import { Controller, Get } from '@nestjs/common';
import { AppService, type ApiInfo } from './app.service';

@Controller()
export class AppController {
    constructor(private readonly appService: AppService) {}

    @Get()
    getHello(): string {
        return this.appService.getHello();
    }

    @Get('info')
    getApiInfo(): ApiInfo {
        return this.appService.getApiInfo();
    }
}

Ahora, si haces curl http://localhost:3000/info obtienes:

{
    "name": "NestJS Course API",
    "version": "1.0.0",
    "description": "API del curso de NestJS desde cero hasta enterprise",
    "status": "running"
}

NestJS ha serializado automáticamente el objeto a JSON y ha puesto el Content-Type: application/json. No has tenido que hacer res.json() ni nada. Devuelves un objeto y NestJS se encarga del resto.


8. Comandos útiles del CLI de NestJS

El es una herramienta brutal que te genera código siguiendo las convenciones del framework. Dentro del contenedor puedes ejecutar:

# Generar un nuevo módulo
docker compose exec api npx nest g module users

# Generar un controller
docker compose exec api npx nest g controller users

# Generar un service
docker compose exec api npx nest g service users

# Generar un CRUD resource completo (controller + service + module + DTOs)
docker compose exec api npx nest g resource users

El comando nest g resource es genial. Te genera un módulo completo con CRUD, DTOs y todo enchufado. Lo veremos en detalle en el próximo post cuando hablemos de Controllers y Routing.


9. Recapitulando

Úsalo cuando...
  • NestJS cuando necesitas estructura y arquitectura en tu backend Node.js
  • Docker desde el minuto 1 para tener un entorno reproducible
  • TypeScript estricto (--strict) para que los tipos trabajen para ti
  • Inyección de dependencias para desacoplar controllers de services
  • El CLI de NestJS para generar código y no reinventar la rueda
Evítalo cuando...
  • Meter lógica de negocio en los controllers (para eso están los services)
  • Instalar Node.js globalmente si puedes usar Docker
  • Usar any en TypeScript. Si necesitas any, algo estás haciendo mal
  • Ignorar la estructura modular. Es tentador meter todo en AppModule, pero no escala
  • Saltarte los tipos de retorno en los métodos. Tipa siempre.

En el próximo post de la serie vamos a meternos de lleno con los Controllers y el Routing. Veremos todos los decoradores de rutas, cómo manejar parámetros, query strings, body, headers, y cómo organizar las rutas de tu API de forma profesional.

EA, nos vemos en los bares!! 🍺


Pon a prueba lo aprendido

1. ¿Qué framework HTTP usa NestJS por debajo de forma predeterminada?

2. ¿Cuál es el decorador que marca una clase como inyectable en el contenedor de dependencias de NestJS?

3. ¿Dónde debe vivir la lógica de negocio en una aplicación NestJS?

4. ¿Qué hace el flag --strict al crear un proyecto con nest new?

5. ¿Por qué usamos un volumen anónimo /app/node_modules en docker-compose?