第48集:RESTful API设计
学习目标
- 了解RESTful API的基本概念和核心原则
- 掌握RESTful API的设计原则和最佳实践
- 学习如何在NestJS中实现RESTful API
- 掌握API文档生成和版本控制的方法
- 能够设计和实现高质量的RESTful API
1. RESTful API基础概念
1.1 什么是REST
REST(Representational State Transfer,表述性状态转移)是一种软件架构风格,由Roy Fielding在2000年的博士论文中提出。REST不是一种协议或标准,而是一组设计原则,用于指导网络应用程序的设计和开发。
1.2 REST的核心原则
- 资源(Resources):一切皆资源,每个资源都有唯一的标识符(URI)
- 表述(Representation):资源可以有多种表述形式,如JSON、XML等
- 状态转移(State Transfer):通过HTTP方法实现资源状态的转移
- 无状态(Stateless):服务器不保存客户端的状态,每次请求都是独立的
- 缓存(Cacheable):响应应该被标记为可缓存或不可缓存
- 分层系统(Layered System):系统可以由多个分层组成,每一层只与相邻层交互
- 统一接口(Uniform Interface):使用统一的接口进行资源操作
1.3 RESTful API的特点
- 使用HTTP标准方法(GET、POST、PUT、PATCH、DELETE等)
- 使用URI标识资源
- 使用HTTP状态码表示操作结果
- 使用JSON或XML作为数据交换格式
- 无状态设计
- 支持缓存
2. RESTful API设计原则
2.1 资源命名原则
2.1.1 资源命名规范
- 使用名词:资源名称应该使用名词,而不是动词
- 使用复数:资源集合应该使用复数形式
- 使用小写:资源名称应该使用小写字母
- 使用连字符:单词之间使用连字符(-)分隔
- 避免使用文件扩展名:不要在URI中包含文件扩展名
示例:
- 正确:
/users、/products、/orders - 错误:
/get-users、/Product、/order.json
2.1.2 资源层级关系
- 使用嵌套URI表示资源之间的层级关系
- 不要创建过深的嵌套层级(建议不超过3层)
- 对于复杂的关系,使用查询参数而不是嵌套URI
示例:
- 正确:
/users/123/orders、/products/456/reviews - 错误:
/users/123/orders/456/items/789
2.2 HTTP方法使用原则
2.2.1 HTTP方法映射
| HTTP方法 | 操作 | 幂等性 | 安全性 |
|---|---|---|---|
| GET | 获取资源 | ✓ | ✓ |
| POST | 创建资源 | ✗ | ✗ |
| PUT | 更新资源 | ✓ | ✗ |
| PATCH | 部分更新资源 | ✓ | ✗ |
| DELETE | 删除资源 | ✓ | ✗ |
| HEAD | 获取资源头部 | ✓ | ✓ |
| OPTIONS | 获取资源支持的方法 | ✓ | ✓ |
2.2.2 HTTP方法使用场景
- GET:获取资源或资源集合
- POST:创建新资源
- PUT:更新整个资源
- PATCH:更新资源的部分内容
- DELETE:删除资源
- HEAD:获取资源的元数据
- OPTIONS:获取资源支持的HTTP方法
示例:
GET /users:获取所有用户GET /users/123:获取ID为123的用户POST /users:创建新用户PUT /users/123:更新ID为123的用户PATCH /users/123:部分更新ID为123的用户DELETE /users/123:删除ID为123的用户
2.3 HTTP状态码使用原则
2.3.1 状态码分类
- 1xx:信息性状态码,表示请求已接收,需要继续处理
- 2xx:成功状态码,表示请求已成功处理
- 3xx:重定向状态码,表示需要进一步操作才能完成请求
- 4xx:客户端错误状态码,表示客户端请求有错误
- 5xx:服务器错误状态码,表示服务器处理请求时出错
2.3.2 常用状态码
成功状态码:
- 200 OK:请求成功
- 201 Created:资源创建成功
- 202 Accepted:请求已接受,但尚未处理
- 204 No Content:请求成功,但无内容返回
客户端错误状态码:
- 400 Bad Request:请求参数错误
- 401 Unauthorized:未授权,需要认证
- 403 Forbidden:已认证,但无权限
- 404 Not Found:资源不存在
- 405 Method Not Allowed:请求方法不允许
- 409 Conflict:请求冲突
- 429 Too Many Requests:请求过多,超出限制
服务器错误状态码:
- 500 Internal Server Error:服务器内部错误
- 501 Not Implemented:请求方法未实现
- 502 Bad Gateway:网关错误
- 503 Service Unavailable:服务不可用
- 504 Gateway Timeout:网关超时
2.4 响应设计原则
2.4.1 成功响应格式
- 单一资源:直接返回资源对象
- 资源集合:返回资源数组,可包含分页信息
- 包含元数据:可在响应中包含元数据,如状态、消息等
示例:
// 单一资源
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
}
// 资源集合
{
"data": [
{
"id": 123,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 124,
"name": "Jane Smith",
"email": "jane@example.com"
}
],
"meta": {
"total": 2,
"page": 1,
"limit": 10
}
}2.4.2 错误响应格式
- 统一错误格式:使用统一的错误响应格式
- 包含错误码:提供具体的错误码
- 包含错误消息:提供详细的错误消息
- 包含错误详情:可包含错误的具体字段和原因
示例:
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid input data",
"details": {
"name": "Name is required",
"email": "Email must be valid"
}
}2.5 HATEOAS原则
HATEOAS(Hypermedia as the Engine of Application State)是REST的核心原则之一,它要求在响应中包含链接,使客户端能够通过这些链接发现和导航资源。
示例:
{
"id": 123,
"name": "John Doe",
"email": "john@example.com",
"_links": {
"self": {
"href": "/users/123"
},
"orders": {
"href": "/users/123/orders"
},
"update": {
"href": "/users/123",
"method": "PUT"
},
"delete": {
"href": "/users/123",
"method": "DELETE"
}
}
}3. NestJS中的RESTful API实现
3.1 控制器设计
3.1.1 基本控制器
在NestJS中,使用@Controller装饰器定义控制器,使用HTTP方法装饰器(如@Get、@Post等)定义路由。
示例:
import { Controller, Get, Post, Put, Patch, Delete, Param, Body, Query } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto } from './dto';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Get()
findAll(@Query() query) {
return this.usersService.findAll(query);
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@Put(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@Patch(':id')
partialUpdate(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.partialUpdate(id, updateUserDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}3.1.2 路由参数
使用@Param装饰器获取路由参数,使用@Query装饰器获取查询参数,使用@Body装饰器获取请求体。
示例:
@Get(':id')
findOne(
@Param('id') id: string, // 路由参数
@Query('fields') fields: string, // 查询参数
) {
return this.usersService.findOne(id, fields);
}
@Post()
create(@Body() createUserDto: CreateUserDto) { // 请求体
return this.usersService.create(createUserDto);
}3.2 服务层设计
服务层负责处理业务逻辑,与数据访问层交互,为控制器提供数据。
示例:
import { Injectable, NotFoundException } from '@nestjs/common';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto } from './dto';
@Injectable()
export class UsersService {
private users: User[] = [];
findAll(query) {
let result = [...this.users];
// 处理查询参数
if (query.name) {
result = result.filter(user => user.name.includes(query.name));
}
if (query.email) {
result = result.filter(user => user.email.includes(query.email));
}
return result;
}
findOne(id: string) {
const user = this.users.find(user => user.id === id);
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
create(createUserDto: CreateUserDto) {
const user: User = {
id: Date.now().toString(),
...createUserDto,
};
this.users.push(user);
return user;
}
update(id: string, updateUserDto: UpdateUserDto) {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) {
throw new NotFoundException(`User with ID ${id} not found`);
}
this.users[index] = { ...this.users[index], ...updateUserDto };
return this.users[index];
}
partialUpdate(id: string, updateUserDto: UpdateUserDto) {
return this.update(id, updateUserDto);
}
remove(id: string) {
const index = this.users.findIndex(user => user.id === id);
if (index === -1) {
throw new NotFoundException(`User with ID ${id} not found`);
}
this.users.splice(index, 1);
return { message: 'User deleted successfully' };
}
}3.3 数据传输对象(DTOs)
使用DTOs定义请求和响应的数据结构,使用类验证器进行数据验证。
示例:
import { IsString, IsEmail, IsOptional } from 'class-validator';
export class CreateUserDto {
@IsString()
name: string;
@IsEmail()
email: string;
@IsString()
password: string;
}
export class UpdateUserDto {
@IsOptional()
@IsString()
name?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
password?: string;
}3.4 异常处理
使用NestJS的异常过滤器处理错误,返回统一的错误响应。
示例:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
const status = exception.getStatus();
const exceptionResponse = exception.getResponse();
response
.status(status)
.json({
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error: exception.name,
message: typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exception.message,
details: (exceptionResponse as any).message?.details || null,
});
}
}4. 路由设计
4.1 基本路由设计
4.1.1 资源路由
- 获取资源集合:
GET /resources - 获取单个资源:
GET /resources/{id} - 创建资源:
POST /resources - 更新资源:
PUT /resources/{id} - 部分更新资源:
PATCH /resources/{id} - 删除资源:
DELETE /resources/{id}
4.1.2 嵌套路由
- 获取关联资源:
GET /resources/{id}/related-resources - 创建关联资源:
POST /resources/{id}/related-resources - 更新关联资源:
PUT /resources/{id}/related-resources/{related-id} - 删除关联资源:
DELETE /resources/{id}/related-resources/{related-id}
4.2 高级路由设计
4.2.1 过滤和排序
使用查询参数实现资源的过滤和排序。
示例:
GET /users?name=John:过滤名称包含John的用户GET /users?sort=name:asc:按名称升序排序GET /users?sort=createdAt:desc:按创建时间降序排序
4.2.2 分页
使用查询参数实现资源的分页。
示例:
GET /users?page=1&limit=10:获取第1页,每页10条记录GET /users?offset=0&limit=10:从第0条记录开始,获取10条记录
4.2.3 字段选择
使用查询参数实现资源字段的选择。
示例:
GET /users?fields=id,name,email:只返回id、name和email字段GET /users/123?fields=name,email:只返回指定用户的name和email字段
5. 请求处理
5.1 请求验证
使用类验证器和管道进行请求验证。
示例:
import { Controller, Post, Body, UsePipes, ValidationPipe } from '@nestjs/common';
import { CreateUserDto } from './dto';
import { UsersService } from './users.service';
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}5.2 请求转换
使用管道进行请求数据的转换。
示例:
import { PipeTransform, Injectable, ArgumentMetadata } from '@nestjs/common';
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string, metadata: ArgumentMetadata): number {
const parsedValue = parseInt(value, 10);
if (isNaN(parsedValue)) {
throw new BadRequestException('Invalid ID format');
}
return parsedValue;
}
}5.3 请求拦截
使用拦截器进行请求的拦截和处理。
示例:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const now = Date.now();
const request = context.switchToHttp().getRequest();
console.log(`[${request.method}] ${request.url} - Start`);
return next
.handle()
.pipe(
tap(() => console.log(`[${request.method}] ${request.url} - End (${Date.now() - now}ms)`)),
);
}
}6. 响应设计
6.1 统一响应格式
创建统一的响应格式,包括成功响应和错误响应。
示例:
// 成功响应
{
"success": true,
"data": { /* 资源数据 */ },
"message": "Operation completed successfully"
}
// 错误响应
{
"success": false,
"error": {
"code": "BAD_REQUEST",
"message": "Invalid input data",
"details": { /* 错误详情 */ }
}
}6.2 分页响应
创建包含分页信息的响应格式。
示例:
{
"success": true,
"data": [ /* 资源数据 */ ],
"meta": {
"total": 100,
"page": 1,
"limit": 10,
"pages": 10
},
"links": {
"self": "http://api.example.com/users?page=1&limit=10",
"next": "http://api.example.com/users?page=2&limit=10",
"prev": null,
"first": "http://api.example.com/users?page=1&limit=10",
"last": "http://api.example.com/users?page=10&limit=10"
}
}6.3 响应缓存
使用缓存拦截器进行响应缓存。
示例:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, CacheKey, CacheTTL } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class CacheInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 实现缓存逻辑
return next.handle();
}
}
// 使用缓存拦截器
@Controller('users')
export class UsersController {
@Get()
@CacheKey('users')
@CacheTTL(60)
findAll() {
return this.usersService.findAll();
}
}7. 错误处理
7.1 异常类型
NestJS提供了多种内置异常类型,也可以创建自定义异常。
内置异常:
BadRequestExceptionUnauthorizedExceptionForbiddenExceptionNotFoundExceptionNotAcceptableExceptionRequestTimeoutExceptionConflictExceptionGoneExceptionPayloadTooLargeExceptionUnprocessableEntityExceptionInternalServerErrorExceptionNotImplementedExceptionBadGatewayExceptionServiceUnavailableExceptionGatewayTimeoutException
自定义异常:
import { HttpException, HttpStatus } from '@nestjs/common';
export class CustomException extends HttpException {
constructor(message: string, statusCode: HttpStatus = HttpStatus.BAD_REQUEST) {
super(message, statusCode);
}
}7.2 全局异常过滤器
创建全局异常过滤器,处理所有类型的异常。
示例:
import { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';
@Catch()
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: any, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse<Response>();
const request = ctx.getRequest<Request>();
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message = 'Internal server error';
let error = 'InternalServerError';
if (exception instanceof HttpException) {
status = exception.getStatus();
error = exception.name;
const exceptionResponse = exception.getResponse();
message = typeof exceptionResponse === 'string'
? exceptionResponse
: (exceptionResponse as any).message || exception.message;
}
response
.status(status)
.json({
success: false,
statusCode: status,
timestamp: new Date().toISOString(),
path: request.url,
error,
message,
});
}
}7.3 错误日志
使用日志服务记录错误信息。
示例:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler, Logger } from '@nestjs/common';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class ErrorLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(ErrorLoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError(error => {
const request = context.switchToHttp().getRequest();
this.logger.error(
`Error handling request ${request.method} ${request.url}`,
error.stack,
);
return throwError(error);
}),
);
}
}8. 版本控制
8.1 URL路径版本控制
在URL路径中包含版本号。
示例:
GET /v1/usersGET /v2/users
实现:
@Controller('v1/users')
export class UsersV1Controller {
// v1版本的实现
}
@Controller('v2/users')
export class UsersV2Controller {
// v2版本的实现
}8.2 请求头版本控制
在请求头中包含版本号。
示例:
GET /users
Accept-Version: 1.0实现:
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class VersionMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const version = req.headers['accept-version'] || '1.0';
req.version = version;
next();
}
}
// 在模块中注册
@Module({
// ...
})
export class AppModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(VersionMiddleware)
.forRoutes('*');
}
}
// 在控制器中使用
@Controller('users')
export class UsersController {
@Get()
findAll(@Req() req) {
const version = req.version;
// 根据版本号返回不同的响应
if (version === '2.0') {
return this.usersService.findAllV2();
}
return this.usersService.findAllV1();
}
}8.3 查询参数版本控制
在查询参数中包含版本号。
示例:
GET /users?version=1.0GET /users?version=2.0
实现:
@Controller('users')
export class UsersController {
@Get()
findAll(@Query('version') version = '1.0') {
// 根据版本号返回不同的响应
if (version === '2.0') {
return this.usersService.findAllV2();
}
return this.usersService.findAllV1();
}
}9. 文档生成
9.1 Swagger/OpenAPI集成
使用@nestjs/swagger包集成Swagger/OpenAPI。
安装:
npm install --save @nestjs/swagger swagger-ui-express配置:
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('API Documentation')
.setDescription('The API description')
.setVersion('1.0')
.addTag('users')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();9.2 文档装饰器
使用Swagger装饰器为API添加文档信息。
示例:
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto, User } from './dto';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({ status: 200, description: 'Return all users', type: [User] })
@ApiResponse({ status: 500, description: 'Internal server error' })
@Get()
findAll() {
return this.usersService.findAll();
}
@ApiOperation({ summary: 'Get user by ID' })
@ApiParam({ name: 'id', description: 'User ID' })
@ApiResponse({ status: 200, description: 'Return user by ID', type: User })
@ApiResponse({ status: 404, description: 'User not found' })
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@ApiOperation({ summary: 'Create user' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, description: 'User created successfully', type: User })
@ApiResponse({ status: 400, description: 'Invalid input data' })
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
}9.3 DTO文档
使用装饰器为DTO添加文档信息。
示例:
import { IsString, IsEmail } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: 'User name', example: 'John Doe' })
@IsString()
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User password', example: 'password123' })
@IsString()
password: string;
}
export class User {
@ApiProperty({ description: 'User ID', example: '123' })
id: string;
@ApiProperty({ description: 'User name', example: 'John Doe' })
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
email: string;
}10. 实战案例
10.1 用户管理API
10.1.1 项目结构
src/
├── users/
│ ├── dto/
│ │ ├── create-user.dto.ts
│ │ ├── update-user.dto.ts
│ │ └── user.dto.ts
│ ├── entities/
│ │ └── user.entity.ts
│ ├── users.controller.ts
│ ├── users.service.ts
│ └── users.module.ts
├── common/
│ ├── filters/
│ │ └── http-exception.filter.ts
│ ├── interceptors/
│ │ ├── logging.interceptor.ts
│ │ └── error-logging.interceptor.ts
│ └── pipes/
│ └── parse-int.pipe.ts
├── app.module.ts
└── main.ts10.1.2 实现代码
1. 用户实体
// src/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn } from 'typeorm';
@Entity()
export class User {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column()
password: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
}2. DTOs
// src/users/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class CreateUserDto {
@ApiProperty({ description: 'User name', example: 'John Doe' })
@IsString()
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
@IsEmail()
email: string;
@ApiProperty({ description: 'User password', example: 'password123', minLength: 6 })
@IsString()
@MinLength(6)
password: string;
}
// src/users/dto/update-user.dto.ts
import { IsOptional, IsString, IsEmail, MinLength } from 'class-validator';
import { ApiPropertyOptional } from '@nestjs/swagger';
export class UpdateUserDto {
@ApiPropertyOptional({ description: 'User name', example: 'John Doe' })
@IsOptional()
@IsString()
name?: string;
@ApiPropertyOptional({ description: 'User email', example: 'john@example.com' })
@IsOptional()
@IsEmail()
email?: string;
@ApiPropertyOptional({ description: 'User password', example: 'password123', minLength: 6 })
@IsOptional()
@IsString()
@MinLength(6)
password?: string;
}
// src/users/dto/user.dto.ts
import { ApiProperty } from '@nestjs/swagger';
export class UserDto {
@ApiProperty({ description: 'User ID', example: '123e4567-e89b-12d3-a456-426614174000' })
id: string;
@ApiProperty({ description: 'User name', example: 'John Doe' })
name: string;
@ApiProperty({ description: 'User email', example: 'john@example.com' })
email: string;
@ApiProperty({ description: 'Creation date', example: '2023-01-01T00:00:00.000Z' })
createdAt: Date;
@ApiProperty({ description: 'Update date', example: '2023-01-01T00:00:00.000Z' })
updatedAt: Date;
}3. 服务
// src/users/users.service.ts
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './entities/user.entity';
import { CreateUserDto, UpdateUserDto } from './dto';
import * as bcrypt from 'bcrypt';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private usersRepository: Repository<User>,
) {}
async findAll(query?: any) {
const { page = 1, limit = 10, name, email } = query;
const skip = (page - 1) * limit;
const queryBuilder = this.usersRepository.createQueryBuilder('user');
if (name) {
queryBuilder.where('user.name LIKE :name', { name: `%${name}%` });
}
if (email) {
queryBuilder.where('user.email LIKE :email', { email: `%${email}%` });
}
const [users, total] = await queryBuilder
.skip(skip)
.take(limit)
.getManyAndCount();
return {
data: users,
meta: {
total,
page: Number(page),
limit: Number(limit),
pages: Math.ceil(total / limit),
},
};
}
async findOne(id: string) {
const user = await this.usersRepository.findOne({ where: { id } });
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async create(createUserDto: CreateUserDto) {
// Check if user already exists
const existingUser = await this.usersRepository.findOne({
where: { email: createUserDto.email }
});
if (existingUser) {
throw new ConflictException('Email already in use');
}
// Hash password
const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
const user = this.usersRepository.create({
...createUserDto,
password: hashedPassword,
});
return await this.usersRepository.save(user);
}
async update(id: string, updateUserDto: UpdateUserDto) {
const user = await this.findOne(id);
// Check if email is already in use by another user
if (updateUserDto.email && updateUserDto.email !== user.email) {
const existingUser = await this.usersRepository.findOne({
where: { email: updateUserDto.email }
});
if (existingUser) {
throw new ConflictException('Email already in use');
}
}
// Hash password if provided
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(updateUserDto.password, 10);
}
Object.assign(user, updateUserDto);
return await this.usersRepository.save(user);
}
async remove(id: string) {
const user = await this.findOne(id);
await this.usersRepository.remove(user);
return { message: 'User deleted successfully' };
}
}4. 控制器
// src/users/users.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, Query, UsePipes, ValidationPipe } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiBody, ApiQuery } from '@nestjs/swagger';
import { UsersService } from './users.service';
import { CreateUserDto, UpdateUserDto, UserDto } from './dto';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private usersService: UsersService) {}
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({ status: 200, description: 'Return all users' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@ApiQuery({ name: 'page', required: false, description: 'Page number', example: 1 })
@ApiQuery({ name: 'limit', required: false, description: 'Limit per page', example: 10 })
@ApiQuery({ name: 'name', required: false, description: 'Filter by name' })
@ApiQuery({ name: 'email', required: false, description: 'Filter by email' })
@Get()
findAll(@Query() query) {
return this.usersService.findAll(query);
}
@ApiOperation({ summary: 'Get user by ID' })
@ApiParam({ name: 'id', description: 'User ID' })
@ApiResponse({ status: 200, description: 'Return user by ID', type: UserDto })
@ApiResponse({ status: 404, description: 'User not found' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
@ApiOperation({ summary: 'Create user' })
@ApiBody({ type: CreateUserDto })
@ApiResponse({ status: 201, description: 'User created successfully', type: UserDto })
@ApiResponse({ status: 400, description: 'Invalid input data' })
@ApiResponse({ status: 409, description: 'Email already in use' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@Post()
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
@ApiOperation({ summary: 'Update user' })
@ApiParam({ name: 'id', description: 'User ID' })
@ApiBody({ type: UpdateUserDto })
@ApiResponse({ status: 200, description: 'User updated successfully', type: UserDto })
@ApiResponse({ status: 400, description: 'Invalid input data' })
@ApiResponse({ status: 404, description: 'User not found' })
@ApiResponse({ status: 409, description: 'Email already in use' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@Put(':id')
@UsePipes(new ValidationPipe({ whitelist: true, transform: true }))
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto);
}
@ApiOperation({ summary: 'Delete user' })
@ApiParam({ name: 'id', description: 'User ID' })
@ApiResponse({ status: 200, description: 'User deleted successfully' })
@ApiResponse({ status: 404, description: 'User not found' })
@ApiResponse({ status: 500, description: 'Internal server error' })
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(id);
}
}5. 模块
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './entities/user.entity';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
@Module({
imports: [TypeOrmModule.forFeature([User])],
controllers: [UsersController],
providers: [UsersService],
exports: [UsersService],
})
export class UsersModule {}6. 主模块
// src/app.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UsersModule } from './users/users.module';
@Module({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'postgres',
database: 'nestjs-api',
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: true,
}),
UsersModule,
],
})
export class AppModule {}7. 主文件
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';
import { ErrorLoggingInterceptor } from './common/interceptors/error-logging.interceptor';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Global filters
app.useGlobalFilters(new HttpExceptionFilter());
// Global interceptors
app.useGlobalInterceptors(new LoggingInterceptor());
app.useGlobalInterceptors(new ErrorLoggingInterceptor());
// Swagger configuration
const config = new DocumentBuilder()
.setTitle('User Management API')
.setDescription('API for managing users')
.setVersion('1.0')
.addTag('users')
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);
await app.listen(3000);
}
bootstrap();11. 最佳实践
11.1 安全性
- 使用HTTPS:保护API通信
- 认证和授权:使用JWT或OAuth进行认证,实现基于角色的授权
- 输入验证:对所有输入进行严格验证
- 密码哈希:使用bcrypt等算法哈希密码
- 防止SQL注入:使用ORM或参数化查询
- 防止XSS攻击:对输出进行转义
- 防止CSRF攻击:使用CSRF令牌
- 限速:防止暴力攻击
11.2 性能
- 缓存:使用Redis等缓存系统
- 分页:对大型数据集使用分页
- 索引:为数据库查询添加适当的索引
- 批量操作:支持批量创建、更新和删除
- 压缩:使用gzip压缩响应
- 异步操作:使用异步处理长时间运行的任务
11.3 可维护性
- 模块化:使用模块化设计
- 代码组织:按照功能组织代码
- 文档:为API添加详细的文档
- 测试:编写单元测试和集成测试
- 错误处理:实现统一的错误处理
- 日志:记录API请求和错误
- 版本控制:实现API版本控制
11.4 可用性
- 状态码:使用正确的HTTP状态码
- 响应格式:使用统一的响应格式
- 错误消息:提供清晰的错误消息
- 健康检查:实现健康检查端点
- 监控:监控API性能和错误
- 备份:定期备份数据
12. 总结
RESTful API设计是现代Web开发的重要组成部分,它遵循一组设计原则和最佳实践,使API更加一致、可预测和易于使用。在NestJS中,我们可以使用其强大的装饰器系统、依赖注入和模块化设计来实现高质量的RESTful API。
本教程介绍了RESTful API的基本概念、设计原则、NestJS中的实现方法、路由设计、请求处理、响应设计、错误处理、版本控制、文档生成、实战案例以及最佳实践等内容。通过学习这些内容,你应该能够设计和实现符合REST原则的高质量API。
在设计RESTful API时,记住以下几点:
- 使用名词表示资源
- 使用HTTP方法表示操作
- 使用HTTP状态码表示结果
- 使用统一的响应格式
- 实现适当的错误处理
- 添加详细的文档
- 考虑安全性、性能和可维护性
通过遵循这些原则和最佳实践,你可以创建出更加健壮、可扩展和用户友好的API。
13. 互动问答
以下哪个不是REST的核心原则?
A. 资源
B. 表述
C. 状态转移
D. 有状态以下哪个HTTP方法是幂等的?
A. POST
B. PUT
C. PATCH
D. DELETE以下哪个状态码表示资源不存在?
A. 400
B. 401
C. 403
D. 404以下哪个路由设计是正确的?
A. GET /get-users
B. POST /users/create
C. PUT /users/123
D. DELETE /delete-user/123以下哪个不是Swagger/OpenAPI的用途?
A. 生成API文档
B. 测试API
C. 生成客户端代码
D. 实现API逻辑
答案:
- D
- B
- D
- C
- D