Cómo conectar NestJS con PostgreSQL usando TypeORM

En la parte 1 armamos una API de tareas que guardaba todo en un arreglo. Funciona, pero cada vez que reinicias el servidor pierdes los datos. Hoy lo arreglamos: conectamos la API a PostgreSQL con TypeORM y las tareas se quedan guardadas.

Parto del proyecto mi-primera-api de la parte anterior. Si no lo tienes, sigue primero ese artículo.

Qué es TypeORM y por qué lo usamos

TypeORM es un ORM: una librería que traduce clases de TypeScript en tablas y filas. Tú escribes repo.find() y ella genera el SELECT. Nest tiene un paquete oficial que la integra con su sistema de módulos, y por eso es la opción más cómoda para empezar.

Paso 1: Levanta PostgreSQL con Docker

No hace falta instalar PostgreSQL en tu máquina. Con Docker lo levantas en un comando:

docker run --name nest-pg \
  -e POSTGRES_PASSWORD=postgres \
  -e POSTGRES_DB=tareas_db \
  -p 5432:5432 \
  -d postgres:16

Eso crea un contenedor llamado nest-pg con una base de datos tareas_db. Para pararlo usa docker stop nest-pg y para volver a prenderlo docker start nest-pg.

Paso 2: Instala las dependencias

npm i @nestjs/typeorm typeorm pg
  • @nestjs/typeorm conecta TypeORM con Nest.
  • typeorm es el ORM.
  • pg es el driver que habla con PostgreSQL.

Paso 3: Configura la conexión

Abre src/app.module.ts y registra la conexión:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TareasModule } from './tareas/tareas.module';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: 'localhost',
      port: 5432,
      username: 'postgres',
      password: 'postgres',
      database: 'tareas_db',
      autoLoadEntities: true,
      synchronize: true,
    }),
    TareasModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Dos opciones merecen explicación:

  • autoLoadEntities: true hace que Nest cargue solo las entidades que registres en cada módulo. No tienes que listarlas aquí.
  • synchronize: true crea y modifica las tablas automáticamente según tus entidades. Es perfecto para desarrollar, pero nunca lo uses en producción, porque un cambio en la entidad puede borrar columnas con datos. En producción se usan migraciones.

Dejé la contraseña escrita en el código para que veas la conexión completa. En la parte 3 la sacamos a un archivo .env.

Paso 4: Convierte la entidad en una tabla

Abre src/tareas/entities/tarea.entity.ts y agrega los decoradores de TypeORM:

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

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

  @Column({ length: 120 })
  titulo: string;

  @Column({ type: 'text', nullable: true })
  descripcion?: string;

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

Cada decorador tiene un trabajo claro:

  • @Entity('tareas') le dice a TypeORM que esta clase es una tabla llamada tareas.
  • @PrimaryGeneratedColumn() crea el id autoincremental.
  • @Column() marca cada campo como columna, y le pasas opciones como el largo máximo o si acepta nulos.

Paso 5: Registra la entidad en el módulo

Abre src/tareas/tareas.module.ts y usa forFeature para decirle a Nest qué entidades usa este módulo:

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TareasService } from './tareas.service';
import { TareasController } from './tareas.controller';
import { Tarea } from './entities/tarea.entity';

@Module({
  imports: [TypeOrmModule.forFeature([Tarea])],
  controllers: [TareasController],
  providers: [TareasService],
})
export class TareasModule {}

Paso 6: Cambia el servicio para usar el repositorio

Ahora el servicio deja de manejar un arreglo y le pide los datos a la base. TypeORM te da un repositorio por cada entidad, y Nest lo inyecta con @InjectRepository.

Reemplaza src/tareas/tareas.service.ts:

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateTareaDto } from './dto/create-tarea.dto';
import { UpdateTareaDto } from './dto/update-tarea.dto';
import { Tarea } from './entities/tarea.entity';

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

  create(createTareaDto: CreateTareaDto): Promise<Tarea> {
    const tarea = this.repo.create(createTareaDto);
    return this.repo.save(tarea);
  }

  findAll(): Promise<Tarea[]> {
    return this.repo.find({ order: { id: 'ASC' } });
  }

  async findOne(id: number): Promise<Tarea> {
    const tarea = await this.repo.findOneBy({ id });
    if (!tarea) {
      throw new NotFoundException(`No existe la tarea con id ${id}`);
    }
    return tarea;
  }

  async update(id: number, updateTareaDto: UpdateTareaDto): Promise<Tarea> {
    const tarea = await this.repo.preload({ id, ...updateTareaDto });
    if (!tarea) {
      throw new NotFoundException(`No existe la tarea con id ${id}`);
    }
    return this.repo.save(tarea);
  }

  async remove(id: number): Promise<void> {
    const tarea = await this.findOne(id);
    await this.repo.remove(tarea);
  }
}

Fíjate en tres métodos del repositorio:

  • create() arma una instancia de Tarea con los datos del DTO. No toca la base todavía.
  • save() inserta o actualiza en la base.
  • preload() busca la tarea por id y le mezcla los campos nuevos. Si no existe, devuelve undefined, y así lanzamos el 404.

El controlador no cambia. Los métodos ahora devuelven promesas, y Nest las espera solo.

Paso 7: Prueba que los datos se guardan

Arranca el servidor:

npm run start:dev

Crea una tarea:

curl -X POST http://localhost:3000/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Guardar en PostgreSQL"}'

Ahora apaga el servidor con Ctrl + C, vuelve a levantarlo y pide la lista:

curl http://localhost:3000/tareas

La tarea sigue ahí. Si quieres ver la tabla por dentro, entra a PostgreSQL desde el contenedor:

docker exec -it nest-pg psql -U postgres -d tareas_db -c "SELECT * FROM tareas;"

Errores comunes

  • ECONNREFUSED 127.0.0.1:5432: el contenedor no está corriendo. Revisa con docker ps y arráncalo con docker start nest-pg.
  • password authentication failed: el usuario o la contraseña del forRoot no coinciden con los del contenedor.
  • Nest can't resolve dependencies of the TareasService: olvidaste TypeOrmModule.forFeature([Tarea]) en el módulo.

Lo que hicimos

  1. Levantamos PostgreSQL con Docker.
  2. Instalamos TypeORM y el driver pg.
  3. Configuramos la conexión en el módulo principal.
  4. Convertimos la entidad en una tabla con decoradores.
  5. Reescribimos el servicio con el repositorio.
  6. Comprobamos que los datos sobreviven a un reinicio.

Lo que sigue

Tenemos usuario y contraseña de la base escritos directo en el código, y eso no se hace. En la parte 3 los movemos a variables de entorno.

⬅️ Anterior: Cómo crear una API REST con NestJS desde cero ➡️ Siguiente: Variables de entorno en NestJS con ConfigModule

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *