Skip to content

HTTP REST Resource

本页对应 nest g resource users 后生成的 src/users。选择 REST API 与 CRUD 入口后,CLI 会生成 Module、Controller、Service、DTO、Entity 及测试文件;它只提供分层骨架,不包含校验、持久化、异常处理或权限控制。

bash
nest g resource users
bash
src/users
├── dto
   ├── create-user.dto.ts
   └── update-user.dto.ts
├── entities
   └── user.entity.ts
├── users.controller.ts
├── users.module.ts
└── users.service.ts
mermaid
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.tsHTTP 路由与参数提取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-validatorclass-transformer

bash
pnpm add class-validator class-transformer
ts
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/1

HTTP 侧重点是资源路径与状态码;身份认证、授权、分页和数据库事务属于在这个骨架上逐层叠加的能力。

参考

基于 MIT 许可发布