Skip to content

CRUD MySQL con NestJS y TypeORM

¡Bienvenidos a esta emocionante serie de tutoriales sobre cómo construir un poderoso CRUD utilizando Nest.js, MySQL, TypeORM y TypeScript! En esta primera entrega, te guiaré paso a paso en la creación de las operaciones básicas de Crear, Leer, Actualizar y Eliminar (CRUD) dentro de una aplicación web.

Nest.js es un marco de trabajo progresivo y eficiente para Node.js, que nos permite construir aplicaciones escalables y robustas utilizando TypeScript. Aprovecharemos la potencia de Nest.js para crear una API RESTful que se conecte a una base de datos MySQL mediante TypeORM, un ORM (Mapeo Objeto-Relacional) que nos simplificará la interacción con la base de datos y la manipulación de nuestros datos.

En esta primera parte, abordaremos la configuración inicial del proyecto, la instalación de las dependencias necesarias y la creación de la estructura básica del servidor con Nest.js. Además, te mostraré cómo configurar la conexión con la base de datos MySQL a través de TypeORM para que podamos empezar a modelar nuestras entidades y definir nuestras relaciones.

Si eres un entusiasta de Node.js y te interesa aprender a desarrollar aplicaciones web con tecnologías modernas y eficientes, esta serie de videos es perfecta para ti. Asegúrate de suscribirte al canal y activar las notificaciones para no perderte ninguna entrega. ¡Vamos a empezar a construir juntos este emocionante proyecto CRUD con Nest.js, MySQL, TypeORM y TypeScript!

Código Github

Ayúdame a seguir creando contenido 😍

Tienes varias jugosas alternativas:

Muchas gracias por su tremendo apoyo 😊

Instalación

sh
nest new project-name

Docker

docker-compose.yml

version: "3.8"
services:
  mysql:
    image: mysql:8.0
    container_name: mysql_db
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: db_crud
      MYSQL_USER: user_crud
      MYSQL_PASSWORD: root
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - "3307:3306"

.gitignore

mysql
docker compose up -d

NestJS

sh
nest g resource cats --no-spec
sh
yarn add class-validator class-transformer -SE

main.ts

ts
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.setGlobalPrefix("api/v1");

  app.useGlobalPipes(
    new ValidationPipe({
      whitelist: true,
      forbidNonWhitelisted: true,
      transform: true,
    })
  );

  await app.listen(3000);
}
bootstrap();

src\cats\cats.controller.ts

ts
@Get(':id')
findOne(@Param('id') id: number) {
  return this.catsService.findOne(id);
}

@Patch(':id')
update(@Param('id') id: number, @Body() updateCatDto: UpdateCatDto) {
  return this.catsService.update(id, updateCatDto);
}

@Delete(':id')
remove(@Param('id') id: number) {
  return this.catsService.remove(id);
}

Variables de entorno

configuration

sh
yarn add @nestjs/config -DE

src\app.module.ts

ts
import { Module } from "@nestjs/common";
import { CatsModule } from "./cats/cats.module";
import { ConfigModule } from "@nestjs/config";

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true }), CatsModule],
  controllers: [],
  providers: [],
})
export class AppModule {}

TypeORM

sh
yarn add @nestjs/typeorm typeorm mysql2

src\app.module.ts

ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CatsModule } from "./cats/cats.module";

@Module({
  imports: [
    CatsModule,
    TypeOrmModule.forRoot({
      type: "mysql",
      host: "localhost",
      port: 3307,
      username: "user_crud",
      password: "root",
      database: "db_crud",
      autoLoadEntities: true,
      synchronize: true,
    }),
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Repository pattern

El Repository Pattern (patrón de repositorio) es un patrón de diseño arquitectónico utilizado en el desarrollo de software para separar la lógica de acceso a datos y la lógica de negocio. Este patrón promueve la abstracción y la modularidad, lo que facilita el mantenimiento y la extensibilidad del código.

El propósito principal del Repository Pattern es proporcionar una interfaz entre la capa de lógica de negocio (servicios, controladores, etc.) y la capa de acceso a datos (base de datos, API externas, etc.). De esta manera, la lógica de negocio no necesita preocuparse por los detalles de cómo se accede y se almacenan los datos, lo que hace que el código sea más desacoplado y flexible.

Características y beneficios del Repository Pattern:

  • Abstracción de la fuente de datos: El patrón de repositorio abstrae el acceso a datos, lo que significa que el código de lógica de negocio no necesita preocuparse por si los datos provienen de una base de datos, una API o cualquier otra fuente.
  • Reutilización de código: Al proporcionar una interfaz común para acceder a los datos, es más fácil reutilizar el código en diferentes partes de la aplicación.
  • Separación de responsabilidades: El patrón de repositorio divide claramente las responsabilidades entre la lógica de negocio y la lógica de acceso a datos, lo que facilita el mantenimiento y la comprensión del código.
  • Facilita las pruebas unitarias: Al utilizar interfaces para representar los repositorios, se pueden crear fácilmente implementaciones de prueba (mocks) para aislar la lógica de negocio durante las pruebas unitarias.
  • Mejora la seguridad: Al controlar el acceso a los datos a través del patrón de repositorio, es más fácil aplicar controles de acceso y medidas de seguridad.

TypeORM Repository pattern

TypeORM admite el patrón de diseño de repositorio, por lo que cada entidad tiene su propio repositorio. Estos repositorios se pueden obtener de la fuente de datos de la base de datos.

src\cats\entities\cat.entity.ts

ts
import {
  Column,
  DeleteDateColumn,
  Entity,
  PrimaryGeneratedColumn,
} from "typeorm";

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;

  @Column()
  breed: string;

  @DeleteDateColumn()
  deletedAt: Date;
}

src\cats\cats.module.ts

ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
import { Cat } from "./entities/cat.entity";

@Module({
  imports: [TypeOrmModule.forFeature([Cat])],
  controllers: [CatsController],
  providers: [CatsService],
})
export class CatsModule {}

src\cats\cats.service.ts

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private catsRepository: Repository<Cat>,
  ) {}

  create(createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  findAll() {
    return this.catsRepository.find();
  }

  findOne(id: number) {
    return this.catsRepository.findOneBy({ id });
  }

  update(id: number, updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  remove(id: number) {
    return this.catsRepository.delete(id);
  }
}

src\cats\dto\create-cat.dto.ts

ts
import { IsInt, IsOptional, IsPositive, IsString } from "class-validator";

export class CreateCatDto {
  @IsString()
  name: string;

  @IsInt()
  @IsPositive()
  age: number;

  @IsString()
  @IsOptional()
  breed: string;
}

CRUD

src\cats\cats.service.ts

ts
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { CreateCatDto } from "./dto/create-cat.dto";
import { UpdateCatDto } from "./dto/update-cat.dto";
import { Cat } from "./entities/cat.entity";

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private catsRepository: Repository<Cat>
  ) {}

  async create(createCatDto: CreateCatDto) {
    const cat = this.catsRepository.create(createCatDto);
    return await this.catsRepository.save(cat);
  }

  async findAll() {
    return await this.catsRepository.find();
  }

  async findOne(id: number) {
    return await this.catsRepository.findOneBy({ id });
  }

  async update(id: number, updateCatDto: UpdateCatDto) {
    return await this.catsRepository.update(id, updateCatDto);
  }

  async remove(id: number) {
    return await this.catsRepository.softDelete(id);
  }
}

En el soft delete, se utiliza una columna especial en la tabla para marcar los registros como "eliminados" sin eliminarlos físicamente de la base de datos. Esta columna, generalmente llamada deletedAt (o el nombre que hayas especificado), almacena la fecha y hora en la que se realizó el soft delete. Los registros marcados con un valor en esta columna se consideran "eliminados" y no son visibles en consultas regulares, pero aún existen en la base de datos y pueden ser recuperados o restaurados si es necesario.

En el soft remove, los registros "eliminados" son movidos a otra tabla especial (por lo general, una tabla histórica o de archivado) en lugar de simplemente marcarlos en la tabla original. De esta manera, los registros eliminados aún se conservan en la base de datos, pero en una tabla diferente, lo que ayuda a mantener la tabla original más liviana y mejora el rendimiento en consultas regulares.

Relaciones

Tipo de RelaciónDescripciónDecorador
One-to-oneCada fila en la tabla principal tiene una y solo una fila asociada en la tabla externa.@OneToOne()
One-to-manyCada fila de la tabla principal tiene una o más filas relacionadas en la tabla externa.@OneToMany()
Many-to-oneCada fila de la tabla principal tiene una o más filas relacionadas en la tabla externa.@ManyToOne()
Many-to-manyCada fila de la tabla principal tiene muchas filas relacionadas en la tabla externa y cada registro de la tabla externa tiene muchas filas relacionadas en la tabla principal.@ManyToMany()

Many-to-one y One-to-many

sh
nest g resource breeds --no-spec

En esta relación, una raza puede estar asociada con muchos gatos. En la entidad Breed, utilizamos @OneToMany() para establecer la relación con la entidad Cat. La propiedad cats en la entidad Breed será una colección de gatos que están asociados con esa raza.

src\breeds\entities\breed.entity.ts

ts
import { Cat } from "src/cats/entities/cat.entity";
import {
  Column,
  DeleteDateColumn,
  Entity,
  OneToMany,
  PrimaryGeneratedColumn,
} from "typeorm";

@Entity()
export class Breed {
  @PrimaryGeneratedColumn()
  id: number;

  @Column({ unique: true })
  name: string;

  @OneToMany(() => Cat, (cat) => cat.breed)
  cats: Cat[];

  @DeleteDateColumn()
  deletedAt: Date;
}

En esta relación, varios gatos pueden pertenecer a una sola raza. En la entidad Cat, utilizamos @ManyToOne() para establecer la relación con la entidad Breed. Un gato tendrá una sola raza, identificada por la propiedad breed en la entidad Cat.

src\cats\entities\cat.entity.ts

ts
import { Breed } from "src/breeds/entities/breed.entity";
import {
  Column,
  DeleteDateColumn,
  Entity,
  ManyToOne,
  PrimaryGeneratedColumn,
} from "typeorm";

@Entity()
export class Cat {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column()
  age: number;

  @ManyToOne(() => Breed, (breed) => breed.id, {
    // cascade: true,
    eager: true, // para que traiga las raza al hacer un findOne
  })
  breed: Breed;

  @DeleteDateColumn()
  deletedAt: Date;
}

Crear Breeds

src\breeds\dto\create-breed.dto.ts

ts
import { IsString, MinLength } from "class-validator";

export class CreateBreedDto {
  @IsString()
  @MinLength(1)
  name: string;
}

src\breeds\breeds.service.ts

ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateBreedDto } from './dto/create-breed.dto';
import { UpdateBreedDto } from './dto/update-breed.dto';
import { Breed } from './entities/breed.entity';

@Injectable()
export class BreedsService {
  constructor(
    @InjectRepository(Breed)
    private readonly breedsRepository: Repository<Breed>,
  ) {}

  async create(createBreedDto: CreateBreedDto) {
    const breed = this.breedsRepository.create(createBreedDto);
    return await this.breedsRepository.save(breed);
  }

  async findAll() {
    return await this.breedsRepository.find();
  }

Comunicación entre módulos

src\breeds\breeds.module.ts

ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { BreedsController } from "./breeds.controller";
import { BreedsService } from "./breeds.service";
import { Breed } from "./entities/breed.entity";

@Module({
  imports: [TypeOrmModule.forFeature([Breed])],
  controllers: [BreedsController],
  providers: [BreedsService],
  exports: [TypeOrmModule],
})
export class BreedsModule {}

src\cats\cats.module.ts

ts
import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import { BreedsModule } from "src/breeds/breeds.module";
import { BreedsService } from "src/breeds/breeds.service";
import { CatsController } from "./cats.controller";
import { CatsService } from "./cats.service";
import { Cat } from "./entities/cat.entity";

@Module({
  imports: [TypeOrmModule.forFeature([Cat]), BreedsModule],
  controllers: [CatsController],
  providers: [CatsService, BreedsService],
  exports: [],
})
export class CatsModule {}

Agregar Cat con Breed

src\cats\cats.service.ts

ts
import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Breed } from 'src/breeds/entities/breed.entity';
import { Repository } from 'typeorm';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
import { Cat } from './entities/cat.entity';

@Injectable()
export class CatsService {
  constructor(
    @InjectRepository(Cat)
    private catsRepository: Repository<Cat>,

    @InjectRepository(Breed)
    private breedsRepository: Repository<Breed>,
  ) {}

  async create(createCatDto: CreateCatDto) {
    const breed = await this.breedsRepository.findOneBy({
      name: createCatDto.breed,
    });

    if (!breed) {
      throw new BadRequestException('Breed not found');
    }

    const cat = this.catsRepository.create({
      name: createCatDto.name,
      age: createCatDto.age,
      breed,
    });
    return await this.catsRepository.save(cat);
  }

Update

src\cats\cats.service.ts

ts
async update(id: number, updateCatDto: UpdateCatDto) {
  const cat = await this.catsRepository.findOneBy({ id });

  if (!cat) {
    throw new BadRequestException('Cat not found');
  }

  let breed;
  if (updateCatDto.breed) {
    breed = await this.breedsRepository.findOneBy({
      name: updateCatDto.breed,
    });

    if (!breed) {
      throw new BadRequestException('Breed not found');
    }
  }

  return await this.catsRepository.save({
    ...cat,
    ...updateCatDto,
    breed,
  });
}