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/typeormconecta TypeORM con Nest.typeormes el ORM.pges 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: truehace que Nest cargue solo las entidades que registres en cada módulo. No tienes que listarlas aquí.synchronize: truecrea 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 llamadatareas.@PrimaryGeneratedColumn()crea elidautoincremental.@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 deTareacon los datos del DTO. No toca la base todavía.save()inserta o actualiza en la base.preload()busca la tarea poridy le mezcla los campos nuevos. Si no existe, devuelveundefined, 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 condocker psy arráncalo condocker start nest-pg.password authentication failed: el usuario o la contraseña delforRootno coinciden con los del contenedor.Nest can't resolve dependencies of the TareasService: olvidasteTypeOrmModule.forFeature([Tarea])en el módulo.
Lo que hicimos
- Levantamos PostgreSQL con Docker.
- Instalamos TypeORM y el driver
pg. - Configuramos la conexión en el módulo principal.
- Convertimos la entidad en una tabla con decoradores.
- Reescribimos el servicio con el repositorio.
- 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
