DI, DTOs, and Professional CRUD
The Heart of NestJS Architecture
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) {}
}
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:
// This service will be re-created for every request
@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
// ...
}
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'
}
Defining the API Contract
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.
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.
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
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;
}
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();
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);
}
}
Using NestJS & TypeORM
We will refactor our `users` resource from our old Express app, rebuilding it with NestJS best practices and connecting it to our TypeORM 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;
}
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 {}
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 ...
}
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 ...
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.