NestJS y TypeORM
Guía detallada para iniciar un proyecto con NestJS y TypeORM.
1. Instalación de TypeORM y dependencias
TypeORM es un ORM (Object-Relational Mapper) que facilita la interacción con bases de datos en aplicaciones Node.js. Para usar TypeORM con NestJS, primero instala las siguientes dependencias:
npm install --save @nestjs/typeorm typeorm @nestjs/config pg
2. Configuración de TypeORM en NestJS
Para configurar TypeORM en tu aplicación NestJS, importa el módulo TypeOrmModule en tu módulo principal (usualmente AppModule). Aquí tienes un ejemplo de configuración básica:
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { UsersModule } from './users/users.module';
import { RolesModule } from './roles/roles.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
type SupportedDbTypes =
| 'mysql'
| 'postgres'
| 'sqlite'
| 'mariadb'
| 'mongodb'
| 'oracle';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
UsersModule,
RolesModule,
TypeOrmModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: configService.get<SupportedDbTypes>('DB_TYPE') ?? 'mysql',
host: configService.get<string>('DB_HOST') ?? 'localhost',
port: configService.get<number>('DB_PORT') ?? 5432,
username: configService.get<string>('DB_USERNAME') ?? 'root',
password: configService.get<string>('DB_PASSWORD') ?? 'root',
database: configService.get<string>('DB_DATABASE') ?? 'test',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: configService.get<boolean>('DB_SYNCHRONIZE') ?? false,
}),
}),
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}
La configuración se realiza de manera asíncrona usando forRootAsync, lo que permite inyectar el ConfigService para obtener las variables de entorno necesarias. Aunque podrías usar process.env directamente, ConfigService es la opción recomendada en NestJS para manejar configuraciones de forma estructurada y segura.
La propiedad synchronize controla si TypeORM sincroniza automáticamente el esquema de la base de datos con tus entidades cada vez que la aplicación inicia. Es útil durante el desarrollo, pero en producción debe estar en false para evitar cambios inesperados en la base de datos.
3. Definición de la entidad Usuario con TypeORM
TypeORM ofrece herramientas similares a JPA en Java. Así defines una entidad User:
import { Role } from 'src/roles/entities/role.entity';
import {
Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
ManyToOne,
JoinColumn,
} from 'typeorm';
@Entity('users')
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
username: string;
@Column({ unique: true })
email: string;
@Column()
passwordHash: string;
@Column({ nullable: true })
bio: string;
@CreateDateColumn()
createdAt: Date;
@ManyToOne(() => Role, (role) => role.users, { eager: true })
@JoinColumn({ name: 'role_id' })
role: Role;
}
Explicación de los decoradores principales:
@Entity('users'): Indica que la clase representa la tablausers.@PrimaryGeneratedColumn(): Columna primaria autogenerada.@Column(): Define una columna en la tabla.@CreateDateColumn(): Fecha de creación automática.@ManyToOne(): Relación muchos a uno con la entidadRole.@JoinColumn(): Especifica la columna de la relación.
4. Definición de la entidad Role con TypeORM
Para cerrar la relación entre usuario y rol, define la entidad Role así:
import { User } from 'src/users/entities/user.entity';
import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from 'typeorm';
@Entity()
export class Role {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
description: string;
@OneToMany(() => User, (user) => user.role)
users: User[];
}
@OneToMany(): Relación uno a muchos con la entidadUser.
5. Modificación del servicio de roles para usar TypeORM
Ahora el servicio de roles utiliza el repositorio de TypeORM en lugar de un arreglo en memoria:
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { Role } from './entities/role.entity';
@Injectable()
export class RolesService {
constructor(
@InjectRepository(Role)
private readonly roleRepository: Repository<Role>,
) {}
async create(createRoleDto: CreateRoleDto): Promise<Role> {
const newRole = this.roleRepository.create(createRoleDto);
return await this.roleRepository.save(newRole);
}
async findAll(): Promise<Role[]> {
return await this.roleRepository.find();
}
async findOne(id: number): Promise<Role | null> {
return await this.roleRepository.findOneBy({ id });
}
async update(
id: number,
updateRoleDto: UpdateRoleDto,
): Promise<Role | null> {
await this.roleRepository.update(id, updateRoleDto);
return await this.roleRepository.findOneBy({ id });
}
async remove(id: number): Promise<{ id: number } | null> {
const result = await this.roleRepository.delete(id);
if (result.affected) {
return { id };
}
return null;
}
async findByName(name: string): Promise<Role | null> {
return await this.roleRepository.findOneBy({ name });
}
}
Ahora puedes aprovechar todas las capacidades de TypeORM para manejar entidades y relaciones.
Métodos avanzados del repositorio de TypeORM
Además de los métodos básicos (create, save, find, findOneBy, update, delete), el repositorio de TypeORM ofrece muchas funcionalidades útiles:
1. find con opciones avanzadas
const users = await userRepository.find({
where: { role: { name: 'admin' } },
order: { createdAt: 'DESC' },
skip: 0,
take: 10,
relations: ['role'],
});
2. findOne y findOneByOrFail
const user = await userRepository.findOne({ where: { email: 'correo@ejemplo.com' } });
const user = await userRepository.findOneByOrFail({ id: 1 });
3. count
const count = await userRepository.count({ where: { role: { name: 'admin' } } });
4. query para SQL personalizado
const rawUsers = await userRepository.query('SELECT * FROM users WHERE email LIKE $1', ['%@gmail.com']);
5. findAndCount
const [users, total] = await userRepository.findAndCount({
where: { role: { name: 'admin' } },
skip: 0,
take: 10,
});
6. save con arreglos
const roles = await roleRepository.save([
{ name: 'admin', description: 'Administrador' },
{ name: 'user', description: 'Usuario estándar' },
]);
7. Relaciones y consultas anidadas
const users = await userRepository.find({
relations: ['role', 'posts', 'posts.comments'],
});
Consulta la documentación oficial de TypeORM para más detalles.
6. Adaptación de los tests del servicio de roles
Para probar el servicio de roles con TypeORM, utiliza un repositorio simulado (mock):
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { RolesService } from './roles.service';
import { Role } from './entities/role.entity';
const mockRepository = {
find: jest.fn(),
findOne: jest.fn(),
save: jest.fn(),
};
describe('RolesService', () => {
let service: RolesService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
RolesService,
{
provide: getRepositoryToken(Role),
useValue: mockRepository,
},
],
}).compile();
service = module.get<RolesService>(RolesService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
Así puedes probar el servicio sin una base de datos real, facilitando las pruebas unitarias.
Para el controlador de roles:
import { Test, TestingModule } from '@nestjs/testing';
import { RolesController } from './roles.controller';
import { RolesService } from './roles.service';
describe('RolesController', () => {
let controller: RolesController;
const mockRolesService = {
create: jest.fn(),
findAll: jest.fn(),
findOne: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [RolesController],
providers: [{ provide: RolesService, useValue: mockRolesService }],
}).compile();
controller = module.get<RolesController>(RolesController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
Se utiliza un mock del servicio para probar el controlador de manera aislada.
7. Modificación del servicio de usuarios para usar TypeORM
El servicio de usuarios también debe usar el repositorio de TypeORM:
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { User } from './entities/user.entity';
import { RolesService } from '../roles/roles.service';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private readonly userRepository: Repository<User>,
private rolesService: RolesService,
) {}
async create(createUserDto: CreateUserDto) {
const role = await this.rolesService.findByName(createUserDto.roleName);
if (!role) {
throw new Error('Role not found');
}
const newUser = this.userRepository.create({
...createUserDto,
role,
});
return await this.userRepository.save(newUser);
}
findAll() {
return this.userRepository.find();
}
findOne(id: number) {
return this.userRepository.findOne({ where: { id } });
}
async update(id: number, updateUserDto: UpdateUserDto) {
await this.userRepository.update(id, updateUserDto);
return this.findOne(id);
}
async remove(id: number) {
const result = await this.userRepository.delete(id);
if (result.affected) {
return { id };
}
return null;
}
}
Los tests para este servicio deben adaptarse de manera similar, usando mocks para los repositorios.
8. Importación de repositorios en los módulos
Asegúrate de importar TypeOrmModule.forFeature en los módulos donde uses los repositorios:
import { Module } from '@nestjs/common';
import { RolesService } from './roles.service';
import { RolesController } from './roles.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Role } from './entities/role.entity';
@Module({
imports: [TypeOrmModule.forFeature([Role])],
controllers: [RolesController],
providers: [RolesService],
exports: [RolesService],
})
export class RolesModule {}
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { RolesModule } from 'src/roles/roles.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [RolesModule, TypeOrmModule.forFeature([User])],
})
export class UsersModule {}
9. Resumen
Para migrar de un sistema en memoria a uno con base de datos relacional usando TypeORM en NestJS, sigue estos pasos:
- Instala las dependencias necesarias (
@nestjs/typeorm,typeorm,@nestjs/config, y el driver de base de datos). - Configura TypeORM en el módulo principal usando
forRootAsyncyConfigService. - Define las entidades (
UseryRole) con los decoradores de TypeORM. - Modifica los servicios para usar repositorios de TypeORM en vez de arreglos en memoria.
- Actualiza los tests para usar mocks de los repositorios.
- Importa los repositorios en los módulos correspondientes con
TypeOrmModule.forFeature. - Maneja correctamente las relaciones entre entidades.
- Utiliza buenas prácticas de configuración con
ConfigService. - Aprovecha los métodos avanzados de los repositorios de TypeORM para consultas y operaciones complejas.
10. Further Reading
11. TODO
- Añadir las entidades de Permisos y la relación muchos a muchos con Roles.
- Añadir la entidad de Games y su relación con Users.