Month 4, Week 2

Building a Production API with NestJS

DI, DTOs, and Professional CRUD

Module 1: Dependency Injection Deep Dive

The Heart of NestJS Architecture

Recap: Inversion of Control (IoC)

Instead of a class creating its own dependencies, it declares them, and the framework (the IoC container) provides them.

This decouples our components, making our application modular and highly testable.


                        // The controller does NOT know how to create the service.
                        // It only knows it NEEDS the service.
                        @Controller('users')
                        export class UserController {
                          constructor(private readonly userService: UserService) {}
                        }
                    

Provider Scopes

By default, a provider is a singleton. NestJS creates one instance of the service and shares it across the entire application.

This is highly efficient. However, you can change this behavior:

  • `Scope.DEFAULT` (Singleton): One instance for the entire application. The standard.
  • `Scope.REQUEST`: A new instance of the service is created for every single incoming request. Useful if you need to manage request-specific state (e.g., caching).
  • `Scope.TRANSIENT`: A new instance is created every time it is injected.

                        // This service will be re-created for every request
                        @Injectable({ scope: Scope.REQUEST })
                        export class RequestScopedService {
                            // ...
                        }
                    

Custom Providers

Sometimes you need to provide a value that isn't a class, like a configuration object or a database connection string.

You can use a "custom provider" to inject non-class values.


                        // In a module file...
                        @Module({
                          providers: [
                            {
                              provide: 'API_CONFIG', // The injection token
                              useValue: { // The actual value to inject
                                apiKey: '123-abc-456',
                                apiVersion: 'v1'
                              }
                            }
                          ],
                        })
                        export class ConfigModule {}

                        // In a service...
                        constructor(@Inject('API_CONFIG') private apiConfig: any) {
                            console.log(this.apiConfig.apiKey); // '123-abc-456'
                        }
                    

Module 2: DTOs & Robust Validation

Defining the API Contract

What is a DTO?

Data Transfer Object. It's an object that defines the shape of data as it moves across different parts of your system, especially over the network.

A DTO is a contract. It tells the client exactly what data the API expects for a request and what data it will return in a response.

The Problem with No Validation

Never trust the client. A request body could be missing required fields, contain fields of the wrong type, or include malicious data.


                        // Client sends this to POST /users
                        {
                            "email": "not-an-email", // Invalid format
                            "age": "twenty-five"     // Wrong type
                            // `name` is missing!
                        }
                    

Without validation, this bad data could crash your service or corrupt your database.

The Solution: `class-validator`

NestJS integrates seamlessly with two powerful libraries: `class-validator` and `class-transformer`.

They allow you to use simple decorators to define complex validation rules directly on your DTO classes.


                        npm install class-validator class-transformer
                    

Creating a Validation DTO

We use decorators from `class-validator` to define the rules.

`src/users/dto/create-user.dto.ts`


                        import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

                        export class CreateUserDto {
                            @IsString()
                            @IsNotEmpty()
                            @MinLength(3)
                            name: string;

                            @IsEmail()
                            @IsNotEmpty()
                            email: string;
                        }
                    

The `ValidationPipe`

A Pipe is a type of middleware in NestJS that can transform or validate incoming data.

NestJS has a built-in `ValidationPipe` that automatically uses `class-validator` on your DTOs.

You apply it globally in your `main.ts` file.


                        // main.ts
                        import { ValidationPipe } from '@nestjs/common';
                        // ...
                        async function bootstrap() {
                          const app = await NestFactory.create(AppModule);
                          
                          app.useGlobalPipes(new ValidationPipe({
                            whitelist: true, // Automatically strip non-whitelisted properties
                          }));

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

Using the DTO

Now, in your controller, you simply type-hint the body with your DTO class.


                        import { CreateUserDto } from './dto/create-user.dto';

                        @Controller('users')
                        export class UserController {
                          constructor(private readonly usersService: UsersService) {}

                          @Post()
                          create(@Body() createUserDto: CreateUserDto) {
                            // If the request body does not match the rules in CreateUserDto,
                            // the ValidationPipe will automatically throw a 400 Bad Request error.
                            // This code will only run if the data is valid.
                            return this.usersService.create(createUserDto);
                          }
                        }
                     

Mid-Lecture Knowledge Check

Module 3: Rebuilding the CRUD API

Using NestJS & TypeORM

The Goal

We will refactor our `users` resource from our old Express app, rebuilding it with NestJS best practices and connecting it to our TypeORM entity.

Step 1: The `User` Entity

We define the `User` entity, which maps our class to the `users` table.

`src/users/entities/user.entity.ts`


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

                        @Entity({ name: 'users' })
                        export class User {
                          @PrimaryGeneratedColumn()
                          id: number;

                          @Column()
                          name: string;

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

Step 2: Injecting the Repository

To interact with the `users` table, we need to inject its TypeORM repository into our `UsersService`.

First, we must import `TypeOrmModule` in our `UsersModule`.


                        // users.module.ts
                        import { TypeOrmModule } from '@nestjs/typeorm';
                        // ...
                        @Module({
                          imports: [TypeOrmModule.forFeature([User])], // Makes User repository available
                          controllers: [UsersController],
                          providers: [UsersService],
                        })
                        export class UsersModule {}
                     

Step 2 (cont'd): Injecting the Repository

Now we can inject it into the service's constructor.


                        // users.service.ts
                        import { Injectable } from '@nestjs/common';
                        import { InjectRepository } from '@nestjs/typeorm';
                        import { Repository } from 'typeorm';
                        import { User } from './entities/user.entity';
                        import { CreateUserDto } from './dto/create-user.dto';

                        @Injectable()
                        export class UsersService {
                          constructor(
                            @InjectRepository(User)
                            private usersRepository: Repository,
                          ) {}
                          
                          // ... business logic methods go here ...
                        }
                     

Step 3: Implementing the Service Logic

Now we replace our old in-memory array logic with TypeORM repository methods.


                        // users.service.ts

                        create(createUserDto: CreateUserDto): Promise {
                          const newUser = this.usersRepository.create(createUserDto);
                          return this.usersRepository.save(newUser);
                        }

                        findAll(): Promise {
                          return this.usersRepository.find();
                        }

                        async findOne(id: number): Promise {
                          const user = await this.usersRepository.findOneBy({ id });
                          if (!user) {
                            // NestJS has built-in exception filters
                            throw new NotFoundException(`User #${id} not found`);
                          }
                          return user;
                        }

                        // ... update and remove methods ...
                     

In-Class Practical Exercise

Implementing the `update` and `remove` Logic

You will be given a nearly complete NestJS CRUD application. Your task is to implement the final two methods in the `UsersService` and their corresponding controller methods.

  1. You will be provided with a NestJS project with `create`, `findAll`, and `findOne` already working.
  2. In `users.service.ts`, implement the `update(id, updateUserDto)` method.
    • First, use the repository to `preload` the user. This finds the user and applies the DTO changes in one step.
    • Check if the user was found. If not, throw a `NotFoundException`.
    • Use `this.usersRepository.save(user)` to persist the changes.
  3. In `users.service.ts`, implement the `remove(id)` method.
    • First, `findOne` to ensure the user exists.
    • If they do, use `this.usersRepository.remove(user)`.
  4. In `users.controller.ts`, make sure the `@Patch(':id')` and `@Delete(':id')` methods call your new service methods.
  5. Test your work using Postman. Ensure you can create, find, update, and then delete a user.

Final Knowledge Check