Database Integration with TypeORM

Connecting to a database (e.g., PostgreSQL, MySQL) using TypeORM. Defining entities, repositories, and performing CRUD operations.


NestJS CRUD Operations with TypeORM

Understanding CRUD Operations

CRUD stands for Create, Read, Update, and Delete. These are the fundamental operations for managing persistent data in any application. In the context of NestJS, we use TypeORM to interact with databases and implement these operations.

  • Create: Adding new data entries to the database.
  • Read: Retrieving existing data entries from the database.
  • Update: Modifying existing data entries in the database.
  • Delete: Removing data entries from the database.

Implementing CRUD with TypeORM in NestJS

This section demonstrates how to implement CRUD operations using TypeORM repositories within NestJS services. This will ensure our application properly persists and retrieves data.

Prerequisites:

  • A NestJS project set up.
  • TypeORM installed and configured (including database connection details).
  • An entity defined (e.g., a User entity with fields like id, name, email).
  • A repository for the entity created (e.g., UserRepository).

Example: User Entity and Repository

First, let's define a simple User entity:

 // user.entity.ts
      import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';

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

        @Column()
        name: string;

        @Column()
        email: string;
      } 

Then, create a repository for it:

 // user.repository.ts
      import { EntityRepository, Repository } from 'typeorm';
      import { User } from './user.entity';

      @EntityRepository(User)
      export class UserRepository extends Repository<User> {} 

Create (POST)

The create operation adds a new user to the database. The service method injects the UserRepository and uses its create and save methods.

 // user.service.ts
      import { Injectable } from '@nestjs/common';
      import { InjectRepository } from '@nestjs/typeorm';
      import { UserRepository } from './user.repository';
      import { User } from './user.entity';

      @Injectable()
      export class UserService {
        constructor(
          @InjectRepository(UserRepository)
          private userRepository: UserRepository,
        ) {}

        async createUser(name: string, email: string): Promise<User> {
          const user = this.userRepository.create({ name, email });
          return this.userRepository.save(user);
        }
      }

      // user.controller.ts
      import { Controller, Post, Body } from '@nestjs/common';
      import { UserService } from './user.service';

      @Controller('users')
      export class UserController {
        constructor(private readonly userService: UserService) {}

        @Post()
        async createUser(@Body() body: { name: string; email: string }) {
          return this.userService.createUser(body.name, body.email);
        }
      } 

Explanation:

  • The createUser method in the service takes the name and email as input.
  • It uses the userRepository.create() method to create a new User entity.
  • The userRepository.save() method persists the new user to the database.
  • The controller receives the request body, which contains name and email. Then it calls the userService.createUser() to persist the data.

Read (GET)

The read operation retrieves existing users from the database. We can read either one, or all users.

 // user.service.ts
      import { Injectable, NotFoundException } from '@nestjs/common';
      import { InjectRepository } from '@nestjs/typeorm';
      import { UserRepository } from './user.repository';
      import { User } from './user.entity';

      @Injectable()
      export class UserService {
        constructor(
          @InjectRepository(UserRepository)
          private userRepository: UserRepository,
        ) {}

        async getAllUsers(): Promise<User[]> {
          return this.userRepository.find();
        }

        async getUserById(id: number): Promise<User> {
           const user = await this.userRepository.findOne(id);
           if (!user) {
            throw new NotFoundException(`User with ID "${id}" not found`);
           }
           return user;
        }
      }

      // user.controller.ts
      import { Controller, Get, Param } from '@nestjs/common';
      import { UserService } from './user.service';

      @Controller('users')
      export class UserController {
        constructor(private readonly userService: UserService) {}

        @Get()
        async getAllUsers() {
          return this.userService.getAllUsers();
        }

        @Get(':id')
        async getUserById(@Param('id') id: string) { // Note: id is a string from params
          return this.userService.getUserById(parseInt(id, 10)); // convert it to a number
        }
      } 

Explanation:

  • The getAllUsers method in the service uses userRepository.find() to retrieve all users.
  • The getUserById method uses userRepository.findOne(id) to retrieve a specific user by ID. Error handling (NotFoundException) is used to handle scenarios where the user is not found.
  • The controller methods handle the routing and calls the respective service methods. The param from the path needs to be parsed into an integer.

Update (PUT/PATCH)

The update operation modifies an existing user in the database.

 // user.service.ts
      import { Injectable, NotFoundException } from '@nestjs/common';
      import { InjectRepository } from '@nestjs/typeorm';
      import { UserRepository } from './user.repository';
      import { User } from './user.entity';

      @Injectable()
      export class UserService {
        constructor(
          @InjectRepository(UserRepository)
          private userRepository: UserRepository,
        ) {}

        async updateUser(id: number, name?: string, email?: string): Promise<User> {
          const user = await this.userRepository.findOne(id);
          if (!user) {
            throw new NotFoundException(`User with ID "${id}" not found`);
          }

          // Optionally update only the fields that are provided in the request.
          if (name) {
            user.name = name;
          }
          if (email) {
            user.email = email;
          }

          return this.userRepository.save(user);
        }
      }

      // user.controller.ts
      import { Controller, Put, Param, Body } from '@nestjs/common';
      import { UserService } from './user.service';

      @Controller('users')
      export class UserController {
        constructor(private readonly userService: UserService) {}

        @Put(':id')
        async updateUser(
          @Param('id') id: string,
          @Body() body: { name?: string; email?: string },
        ) {
          return this.userService.updateUser(parseInt(id, 10), body.name, body.email);
        }
      } 

Explanation:

  • The updateUser method in the service first finds the user by ID using userRepository.findOne(id).
  • If the user is not found, a NotFoundException is thrown.
  • Then, it updates the user's properties (name, email) with the new values provided in the request body, only if they are provided. This allows for partial updates.
  • Finally, it saves the updated user to the database using userRepository.save(user).
  • The Controller method handles the route and parses the parameters before calling the update method.

Delete (DELETE)

The delete operation removes a user from the database.

 // user.service.ts
      import { Injectable, NotFoundException } from '@nestjs/common';
      import { InjectRepository } from '@nestjs/typeorm';
      import { UserRepository } from './user.repository';

      @Injectable()
      export class UserService {
        constructor(
          @InjectRepository(UserRepository)
          private userRepository: UserRepository,
        ) {}

        async deleteUser(id: number): Promise<void> {
          const user = await this.userRepository.findOne(id);
          if (!user) {
            throw new NotFoundException(`User with ID "${id}" not found`);
          }

          await this.userRepository.remove(user);  // or userRepository.delete(id);
        }
      }

      // user.controller.ts
      import { Controller, Delete, Param, HttpCode, HttpStatus } from '@nestjs/common';
      import { UserService } from './user.service';

      @Controller('users')
      export class UserController {
        constructor(private readonly userService: UserService) {}

        @Delete(':id')
        @HttpCode(HttpStatus.NO_CONTENT) // 204 No Content - resource deleted
        async deleteUser(@Param('id') id: string): Promise<void> {
          await this.userService.deleteUser(parseInt(id, 10));
        }
      } 

Explanation:

  • The deleteUser method in the service first finds the user by ID.
  • If the user is not found, a NotFoundException is thrown.
  • Then, it removes the user from the database using userRepository.remove(user) or userRepository.delete(id). The remove method requires fetching the entity first. The delete(id) method directly deletes without fetching.
  • The Controller's delete method calls the delete method of the service and sends a 204 No Content status back.

Conclusion

This example demonstrates how to implement CRUD operations with TypeORM in NestJS. By using TypeORM repositories, you can easily interact with your database and perform common data management tasks. Remember to adjust the code to fit your specific entity and application requirements. Always handle errors appropriately and consider implementing input validation to ensure data integrity. Consider using DTOs (Data Transfer Objects) to structure your request and response data.