Trong suốt quá trình sống của một project, khi xây các feature mới, chúng ta thường cần thêm resource mới vào ứng dụng. Các resource này thường yêu cầu nhiều thao tác lặp đi lặp lại, mà ta phải làm lại mỗi khi định nghĩa một resource mới.
Giới thiệu
Hãy tưởng tượng một tình huống thực tế, khi ta cần expose các endpoint CRUD cho 2 entity, giả sử là User và Product. Theo best practice, với mỗi entity ta sẽ phải thực hiện nhiều thao tác, như sau:
- Sinh một module (
nest g mo) để giữ code có tổ chức và thiết lập ranh giới rõ ràng (nhóm các component liên quan) - Sinh một controller (
nest g co) để định nghĩa route CRUD (hoặc query/mutation cho ứng dụng GraphQL) - Sinh một service (
nest g s) để triển khai & tách biệt business logic - Sinh một entity class/interface để đại diện cho cấu trúc dữ liệu của resource
- Sinh Data Transfer Object (hoặc input cho ứng dụng GraphQL) để định nghĩa cách dữ liệu được gửi qua network
Đúng là rất nhiều bước!
Để giúp tăng tốc quá trình lặp đi lặp lại này, Nest CLI cung cấp một generator (schematic) tự động sinh toàn bộ code khung (boilerplate), giúp bạn không phải làm tất cả những việc trên, và làm cho trải nghiệm phát triển đơn giản hơn nhiều.
Lưu ý Schematic này hỗ trợ sinh controller HTTP, controller Microservice, resolver GraphQL (cả code first và schema first), và WebSocket Gateway.
Sinh một resource mới
Để tạo một resource mới, chỉ cần chạy lệnh sau ở thư mục gốc của project:
$ nest g resource
Lệnh nest g resource không chỉ sinh tất cả các building block của NestJS (class module, service, controller) mà còn sinh cả entity class, DTO class cũng như các file test (.spec).
Dưới đây là file controller được sinh ra (cho REST API):
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Get()
findAll() {
return this.usersService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(+id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
}
Ngoài ra, nó tự động tạo sẵn placeholder cho tất cả endpoint CRUD (route cho REST API, query và mutation cho GraphQL, message subscribe cho cả Microservice và WebSocket Gateway) - tất cả mà không cần bạn động tay.
Lưu ý Các class service được sinh ra không gắn với bất kỳ ORM (hay nguồn dữ liệu) cụ thể nào. Điều này giúp generator đủ tổng quát để đáp ứng nhu cầu của mọi project. Theo mặc định, tất cả phương thức sẽ chứa placeholder, cho phép bạn tự điền nguồn dữ liệu phù hợp với project của mình.
Ghi chú người dịch Đây là điểm khác biệt lớn so với generator của các framework khác (ví dụ php artisan make:model của Laravel thường sinh sẵn code gắn với Eloquent, hay một số scaffold tool của Go gắn cứng với GORM). NestJS CLI cố tình sinh service “rỗng” (chỉ có placeholder), vì Nest không ép bạn dùng ORM nào cụ thể — bạn phải tự viết phần gọi TypeORM/Prisma/Mongoose vào trong các phương thức đó. Nếu bạn mong đợi code sinh ra chạy được ngay như của Laravel, đây là điều dễ gây bất ngờ.
Tương tự, nếu bạn muốn sinh resolver cho ứng dụng GraphQL, chỉ cần chọn GraphQL (code first) (hoặc GraphQL (schema first)) làm transport layer.
Trong trường hợp này, NestJS sẽ sinh ra một resolver class thay vì controller REST API:
$ nest g resource users
> ? What transport layer do you use? GraphQL (code first)
> ? Would you like to generate CRUD entry points? Yes
> CREATE src/users/users.module.ts (224 bytes)
> CREATE src/users/users.resolver.spec.ts (525 bytes)
> CREATE src/users/users.resolver.ts (1109 bytes)
> CREATE src/users/users.service.spec.ts (453 bytes)
> CREATE src/users/users.service.ts (625 bytes)
> CREATE src/users/dto/create-user.input.ts (195 bytes)
> CREATE src/users/dto/update-user.input.ts (281 bytes)
> CREATE src/users/entities/user.entity.ts (187 bytes)
> UPDATE src/app.module.ts (312 bytes)
Gợi ý Để tránh sinh file test, bạn có thể truyền flag --no-spec, như sau: nest g resource users --no-spec
Có thể thấy, không chỉ toàn bộ mutation và query khung được tạo ra, mà mọi thứ còn được liên kết chặt chẽ với nhau. Chúng ta đang dùng UsersService, entity User, và các DTO của mình.
import { Resolver, Query, Mutation, Args, Int } from '@nestjs/graphql';
import { UsersService } from './users.service.js';
import { User } from './entities/user.entity.js';
import { CreateUserInput } from './dto/create-user.input.js';
import { UpdateUserInput } from './dto/update-user.input.js';
@Resolver(() => User)
export class UsersResolver {
constructor(private readonly usersService: UsersService) {}
@Mutation(() => User)
createUser(@Args('createUserInput') createUserInput: CreateUserInput) {
return this.usersService.create(createUserInput);
}
@Query(() => [User], { name: 'users' })
findAll() {
return this.usersService.findAll();
}
@Query(() => User, { name: 'user' })
findOne(@Args('id', { type: () => Int }) id: number) {
return this.usersService.findOne(id);
}
@Mutation(() => User)
updateUser(@Args('updateUserInput') updateUserInput: UpdateUserInput) {
return this.usersService.update(updateUserInput.id, updateUserInput);
}
@Mutation(() => User)
removeUser(@Args('id', { type: () => Int }) id: number) {
return this.usersService.remove(id);
}
}