主题
HTTP REST Resource
本页对应 nest g resource users 后生成的 src/users。选择 REST API 与 CRUD 入口后,CLI 会生成 Module、Controller、Service、DTO、Entity 及测试文件;它只提供分层骨架,不包含校验、持久化、异常处理或权限控制。
bash
nest g resource usersbash
src/users
├── dto
│ ├── create-user.dto.ts
│ └── update-user.dto.ts
├── entities
│ └── user.entity.ts
├── users.controller.ts
├── users.module.ts
└── users.service.tsmermaid
flowchart LR
Client[HTTP 客户端] --> Controller[UsersController]
Controller --> Pipe[ValidationPipe / ParseIntPipe]
Pipe --> Service[UsersService]
Service --> Repository[Repository 或 ORM]
Repository --> Service
Service --> Controller
Controller --> Client生成代码的职责
| 文件 | 生成后的职责 | 实际项目补充 |
|---|---|---|
users.module.ts | 注册 Controller、Service | 导入数据库、认证等依赖模块 |
users.controller.ts | HTTP 路由与参数提取 | Pipe、Guard、返回状态码 |
users.service.ts | 用例入口 | 业务规则、事务、持久化调用 |
dto/*.ts | 输入类型骨架 | class-validator 校验规则 |
entities/user.entity.ts | 资源形状占位 | ORM Model、领域实体或响应 DTO |
你的 main.ts 已设置 app.setGlobalPrefix('api'),因此 Controller 的 @Controller('users') 实际对应 /api/users。
可运行的内存版示例
以下示例用内存 Map 展示完整结构;生产环境将 Service 内的存储替换为 Prisma、Drizzle 或 Repository,而不是让 Controller 直接访问数据库。
1. 入口:全局校验
DTO 校验需要 class-validator 与 class-transformer:
bash
pnpm add class-validator class-transformerts
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')
app.useGlobalPipes(
new ValidationPipe({
whitelist: true,
forbidNonWhitelisted: true,
transform: true,
}),
)
await app.listen(process.env.PORT ?? 3000)
}
void bootstrap()2. DTO 与资源形状
ts
// dto/create-user.dto.ts
import { IsEmail, IsString, MaxLength, MinLength } from 'class-validator'
export class CreateUserDto {
@IsString()
@MinLength(1)
@MaxLength(64)
name!: string
@IsEmail()
email!: string
}ts
// dto/update-user.dto.ts
import { PartialType } from '@nestjs/mapped-types'
import { CreateUserDto } from './create-user.dto'
export class UpdateUserDto extends PartialType(CreateUserDto) {}ts
// entities/user.entity.ts
export class User {
id!: number
name!: string
email!: string
}PartialType 将创建 DTO 的字段变为可选,适合部分更新。它描述输入契约,不等同于数据库实体;密码哈希、内部标记等字段不能直接暴露在响应 DTO 中。
3. Service:业务与数据访问边界
ts
import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { User } from './entities/user.entity'
@Injectable()
export class UsersService {
private readonly users = new Map<number, User>()
private nextId = 1
create(input: CreateUserDto): User {
if ([...this.users.values()].some((user) => user.email === input.email)) {
throw new ConflictException('邮箱已被使用')
}
const user: User = { id: this.nextId++, ...input }
this.users.set(user.id, user)
return user
}
findAll(): User[] {
return [...this.users.values()]
}
findOne(id: number): User {
const user = this.users.get(id)
if (!user) throw new NotFoundException('用户不存在')
return user
}
update(id: number, input: UpdateUserDto): User {
const user = this.findOne(id)
const updated = { ...user, ...input }
this.users.set(id, updated)
return updated
}
remove(id: number): void {
this.findOne(id)
this.users.delete(id)
}
}真实数据库仍需对 email 建立唯一约束;Service 中的预检查只用于更友好的错误信息,不能替代并发安全约束。
4. Controller:路由与输入转换
ts
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseIntPipe, Patch, Post } from '@nestjs/common'
import { CreateUserDto } from './dto/create-user.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { UsersService } from './users.service'
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Post()
create(@Body() input: CreateUserDto) {
return this.usersService.create(input)
}
@Get()
findAll() {
return this.usersService.findAll()
}
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.usersService.findOne(id)
}
@Patch(':id')
update(@Param('id', ParseIntPipe) id: number, @Body() input: UpdateUserDto) {
return this.usersService.update(id, input)
}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number): void {
this.usersService.remove(id)
}
}参考源码中的 +id 会进行 JavaScript 数字转换,但 ParseIntPipe 能在进入方法前拒绝无效参数并返回 400。参考源码使用 @Put(':id'),同时更新 DTO 又是 PartialType;本例改用 PATCH 以表达部分更新。若接口约定为完整替换,可改用 PUT,并使用字段齐全的 DTO。
5. Module:注册 Provider
ts
import { Module } from '@nestjs/common'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}AppModule 导入 UsersModule 后,Nest 才能发现这个功能模块。Controller 通过构造函数取得 Service,Service 则可继续注入 Repository 或 ORM Provider。
调用结果
bash
curl -X POST http://localhost:3000/api/users \
-H 'content-type: application/json' \
-d '{"name":"Ada","email":"ada@example.com"}'
curl http://localhost:3000/api/users/1
curl -X PATCH http://localhost:3000/api/users/1 \
-H 'content-type: application/json' \
-d '{"name":"Ada Lovelace"}'
curl -i -X DELETE http://localhost:3000/api/users/1HTTP 侧重点是资源路径与状态码;身份认证、授权、分页和数据库事务属于在这个骨架上逐层叠加的能力。
