RESTful API 设计规范
大约 10 分钟约 3051 字
RESTful API 设计规范
简介
良好的 API 设计直接影响系统的可维护性和团队协作效率。本篇总结 RESTful API 设计的最佳实践,涵盖 URL 命名、HTTP 方法、状态码、分页、版本控制和错误处理等核心规范。
特点
URL 设计
资源命名规范
# 资源用复数名词
GET /api/users 获取用户列表
GET /api/users/123 获取单个用户
POST /api/users 创建用户
PUT /api/users/123 更新用户(全量)
PATCH /api/users/123 更新用户(部分)
DELETE /api/users/123 删除用户
# 嵌套资源
GET /api/users/123/orders 用户的订单列表
GET /api/users/123/orders/456 用户的特定订单
POST /api/users/123/orders 为用户创建订单
# 避免动词(错误示例)
GET /api/getUsers ❌
POST /api/createUser ❌
POST /api/deleteUser/123 ❌
# 特殊动作可用动词(非 CRUD 操作)
POST /api/users/123/activate 激活用户
POST /api/orders/456/cancel 取消订单
POST /api/files/upload 上传文件
POST /api/auth/login 登录查询参数
# 筛选
GET /api/users?status=active&role=admin
GET /api/products?minPrice=100&maxPrice=500
# 排序
GET /api/users?sort=createdAt&order=desc
GET /api/products?sort=price,name&order=asc,desc
# 字段选择
GET /api/users?fields=id,name,email
# 搜索
GET /api/users?keyword=张三
# 嵌套资源展开
GET /api/orders/456?expand=user,itemsHTTP 方法与状态码
方法对照
| 方法 | 用途 | 幂等 | 安全 | 成功状态码 |
|---|---|---|---|---|
| GET | 获取资源 | 是 | 是 | 200 OK |
| POST | 创建资源 | 否 | 否 | 201 Created |
| PUT | 全量更新 | 是 | 否 | 200 OK |
| PATCH | 部分更新 | 否 | 否 | 200 OK |
| DELETE | 删除资源 | 是 | 否 | 204 No Content |
状态码使用
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
public IActionResult GetAll([FromQuery] QueryParams query)
{
var result = _service.GetAll(query);
return Ok(result); // 200
}
[HttpGet("{id}")]
public async Task<IActionResult> GetById(int id)
{
var product = await _service.GetByIdAsync(id);
if (product == null)
return NotFound(new { message = $"产品 {id} 不存在" }); // 404
return Ok(product); // 200
}
[HttpPost]
public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
{
if (!ModelState.IsValid)
return BadRequest(ModelState); // 400
var product = await _service.CreateAsync(request);
return CreatedAtAction( // 201
nameof(GetById),
new { id = product.Id },
product
);
}
[HttpPut("{id}")]
public async Task<IActionResult> Update(int id, [FromBody] UpdateProductRequest request)
{
var existing = await _service.GetByIdAsync(id);
if (existing == null)
return NotFound(); // 404
await _service.UpdateAsync(id, request);
return NoContent(); // 204
}
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(int id)
{
await _service.DeleteAsync(id);
return NoContent(); // 204
}
}统一响应格式
标准响应
// 统一响应模型
public class ApiResponse<T>
{
public int Code { get; set; }
public string Message { get; set; } = "";
public T? Data { get; set; }
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
public static ApiResponse<T> Success(T data, string message = "success")
=> new() { Code = 0, Message = message, Data = data };
public static ApiResponse<T> Error(int code, string message)
=> new() { Code = code, Message = message };
}
// 分页响应
public class PagedResponse<T> : ApiResponse<List<T>>
{
public int Page { get; set; }
public int Size { get; set; }
public long Total { get; set; }
public int TotalPages => (int)Math.Ceiling(Total / (double)Size);
public bool HasNext => Page < TotalPages;
public bool HasPrev => Page > 1;
}
// 错误响应
public class ErrorResponse
{
public string TraceId { get; set; } = "";
public List<ValidationError> Errors { get; set; } = new();
}
public class ValidationError
{
public string Field { get; set; } = "";
public string Message { get; set; } = "";
}全局异常处理
public class ApiExceptionMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
try
{
await next(context);
}
catch (ValidationException ex)
{
context.Response.StatusCode = 400;
await context.Response.WriteAsJsonAsync(new
{
Code = 400,
Message = "验证失败",
Errors = ex.Errors.Select(e => new { e.Field, e.Message })
});
}
catch (NotFoundException ex)
{
context.Response.StatusCode = 404;
await context.Response.WriteAsJsonAsync(new
{
Code = 404,
Message = ex.Message
});
}
catch (BusinessException ex)
{
context.Response.StatusCode = 422;
await context.Response.WriteAsJsonAsync(new
{
Code = ex.Code,
Message = ex.Message
});
}
catch (Exception ex)
{
context.Response.StatusCode = 500;
await context.Response.WriteAsJsonAsync(new
{
Code = 500,
Message = "服务器内部错误",
TraceId = context.TraceIdentifier
});
}
}
}分页设计
分页参数
public class PaginationParams
{
private int _page = 1;
private int _size = 20;
public int Page
{
get => _page;
set => _page = Math.Max(1, value);
}
public int Size
{
get => _size;
set => _size = Math.Clamp(value, 1, 100); // 限制最大100
}
public string? Sort { get; set; }
public string? Order { get; set; } = "asc";
}
// 控制器
[HttpGet]
public async Task<IActionResult> GetAll([FromQuery] PaginationParams pagination)
{
var query = _context.Products.AsQueryable();
// 排序
query = pagination.Sort?.ToLower() switch
{
"price" => pagination.Order == "desc" ? query.OrderByDescending(p => p.Price)
: query.OrderBy(p => p.Price),
"name" => pagination.Order == "desc" ? query.OrderByDescending(p => p.Name)
: query.OrderBy(p => p.Name),
_ => query.OrderBy(p => p.Id)
};
var total = await query.LongCountAsync();
var items = await query
.Skip((pagination.Page - 1) * pagination.Size)
.Take(pagination.Size)
.ToListAsync();
return Ok(new PagedResponse<Product>
{
Data = items,
Page = pagination.Page,
Size = pagination.Size,
Total = total
});
}HATEOAS 与超媒体
超媒体响应设计
// HATEOAS(Hypermedia as the Engine of Application State)
// 响应中包含相关操作的链接,客户端无需硬编码 URL
public class LinkDto
{
public string Href { get; set; } = string.Empty;
public string Rel { get; set; } = string.Empty;
public string Method { get; set; } = "GET";
}
public class ProductWithLinks
{
public long Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
public List<LinkDto> Links { get; set; } = new();
}
// 在 Controller 中生成链接
[ApiController]
[Route("api/v1/products")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public async Task<ActionResult<ProductWithLinks>> Get(int id)
{
var product = await _service.GetByIdAsync(id);
if (product == null) return NotFound();
var result = new ProductWithLinks
{
Id = product.Id,
Name = product.Name,
Price = product.Price,
Links = new List<LinkDto>
{
new() { Rel = "self", Href = $"/api/v1/products/{id}", Method = "GET" },
new() { Rel = "update", Href = $"/api/v1/products/{id}", Method = "PUT" },
new() { Rel = "delete", Href = $"/api/v1/products/{id}", Method = "DELETE" },
new() { Rel = "category", Href = $"/api/v1/categories/{product.CategoryId}", Method = "GET" },
new() { Rel = "reviews", Href = $"/api/v1/products/{id}/reviews", Method = "GET" }
}
};
return Ok(result);
}
[HttpGet]
public async Task<ActionResult<PagedResponse<ProductWithLinks>>> List(
[FromQuery] int page = 1, [FromQuery] int size = 20)
{
var (items, total) = await _service.GetPagedAsync(page, size);
var result = new PagedResponse<ProductWithLinks>
{
Data = items.Select(p => new ProductWithLinks
{
Id = p.Id,
Name = p.Name,
Price = p.Price,
Links = new List<LinkDto>
{
new() { Rel = "self", Href = $"/api/v1/products/{p.Id}", Method = "GET" }
}
}).ToList(),
Page = page,
Size = size,
Total = total,
Links = new List<LinkDto>
{
new() { Rel = "self", Href = $"/api/v1/products?page={page}&size={size}", Method = "GET" },
new() { Rel = "next", Href = $"/api/v1/products?page={page + 1}&size={size}", Method = "GET" },
new() { Rel = "prev", Href = $"/api/v1/products?page={Math.Max(1, page - 1)}&size={size}", Method = "GET" }
}
};
return Ok(result);
}
}批量操作与部分更新
批量 API 设计
// 批量创建
[HttpPost("batch")]
public async Task<ActionResult<BatchResult>> CreateBatch(
[FromBody] List<CreateProductRequest> requests)
{
if (requests.Count > 100)
return BadRequest("单次批量操作不能超过 100 条");
var results = new List<BatchItemResult>();
foreach (var request in requests)
{
try
{
var id = await _service.CreateAsync(request);
results.Add(new BatchItemResult { Index = requests.IndexOf(request),
Success = true, Id = id });
}
catch (Exception ex)
{
results.Add(new BatchItemResult { Index = requests.IndexOf(request),
Success = false, Error = ex.Message });
}
}
return Ok(new BatchResult
{
Total = requests.Count,
Succeeded = results.Count(r => r.Success),
Failed = results.Count(r => !r.Success),
Items = results
});
}
public record BatchResult(int Total, int Succeeded, int Failed, List<BatchItemResult> Items);
public record BatchItemResult(int Index, bool Success, long? Id = null, string? Error = null);
// 部分更新(PATCH)
[HttpPatch("{id}")]
public async Task<IActionResult> PartialUpdate(
int id, [FromBody] JsonElement patch)
{
var product = await _service.GetByIdAsync(id);
if (product == null) return NotFound();
// 只更新提供的字段
if (patch.TryGetProperty("name", out var name))
product.Name = name.GetString()!;
if (patch.TryGetProperty("price", out var price))
product.Price = price.GetDecimal();
await _service.UpdateAsync(product);
return NoContent();
}
// 使用 JsonPatch(RFC 6902)
[HttpPatch("{id}/json-patch")]
public async Task<IActionResult> JsonPatchUpdate(
int id, [FromBody] JsonPatchDocument<ProductDto> patchDoc)
{
var product = await _service.GetByIdAsync(id);
if (product == null) return NotFound();
var dto = Mapper.Map<ProductDto>(product);
patchDoc.ApplyTo(dto, ModelState);
if (!ModelState.IsValid) return ValidationProblem(ModelState);
await _service.UpdateAsync(Mapper.Map<Product>(dto));
return NoContent();
}异步操作 API
// 长时间操作使用异步 API 模式
[HttpPost("export")]
public async Task<ActionResult<AsyncOperationResult>> StartExport()
{
var operationId = Guid.NewGuid();
var location = $"/api/v1/products/export/{operationId}/status";
// 启动后台任务
_ = Task.Run(async () =>
{
try
{
await _exportService.ExportAsync(operationId);
}
catch (Exception ex)
{
await _operationStore.SetFailedAsync(operationId, ex.Message);
}
});
return Accepted(new AsyncOperationResult
{
OperationId = operationId,
Status = "processing",
Location = location,
EstimatedCompletionSeconds = 120
});
}
// 查询操作状态
[HttpGet("export/{operationId}/status")]
public async Task<ActionResult<AsyncOperationStatus>> GetStatus(Guid operationId)
{
var status = await _operationStore.GetAsync(operationId);
if (status == null) return NotFound();
Response.Headers["Location"] = status.DownloadUrl;
return Ok(status);
}
// 下载结果
[HttpGet("export/{operationId}/download")]
public async Task<IActionResult> Download(Guid operationId)
{
var status = await _operationStore.GetAsync(operationId);
if (status?.Status != "completed") return NotFound("导出尚未完成");
var fileBytes = await _exportService.GetResultAsync(operationId);
return File(fileBytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
$"export-{operationId}.xlsx");
}
public record AsyncOperationResult(
Guid OperationId, string Status, string Location, int EstimatedCompletionSeconds);
public record AsyncOperationStatus(
Guid OperationId, string Status, int? Progress, string? DownloadUrl);API 限流与配额
限流头设计
// 限流响应头(遵循 RateLimit 规范)
// X-RateLimit-Limit: 100 — 窗口期内最大请求数
// X-RateLimit-Remaining: 95 — 剩余请求数
// X-RateLimit-Reset: 1712900000 — 窗口重置时间(Unix 时间戳)
// Retry-After: 30 — 超限后等待秒数
app.Use(async (context, next) =>
{
var clientId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
var rateLimitKey = $"rate:{clientId}";
var db = context.RequestServices.GetRequiredService<IDatabase>();
var count = await db.StringIncrementAsync(rateLimitKey);
if (count == 1)
await db.KeyExpireAsync(rateLimitKey, TimeSpan.FromMinutes(1));
var limit = 100;
context.Response.Headers["X-RateLimit-Limit"] = limit.ToString();
context.Response.Headers["X-RateLimit-Remaining"] =
Math.Max(0, limit - (int)count).ToString();
if (count > limit)
{
var ttl = await db.KeyTimeToLiveAsync(rateLimitKey);
context.Response.Headers["Retry-After"] = ttl?.Seconds.ToString() ?? "60";
context.Response.StatusCode = 429;
await context.Response.WriteAsJsonAsync(new
{
error = "Too Many Requests",
message = $"请求频率超过限制({limit}/分钟),请稍后重试",
retryAfter = ttl?.Seconds ?? 60
});
return;
}
await next();
});API 契约与文档
OpenAPI 增强
// 添加详细的 API 描述和示例
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "产品管理 API",
Contact = new OpenApiContact
{
Name = "API Support",
Email = "api@example.com"
}
});
// 为所有接口添加通用响应
options.OperationFilter<CommonResponsesFilter>();
// XML 注释支持
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
});
// 通用响应过滤器
public class CommonResponsesFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
operation.Responses.TryAdd("401", new OpenApiResponse
{
Description = "未认证 — 缺少或无效的认证令牌"
});
operation.Responses.TryAdd("403", new OpenApiResponse
{
Description = "无权限 — 当前用户没有执行此操作的权限"
});
operation.Responses.TryAdd("429", new OpenApiResponse
{
Description = "请求过于频繁 — 已超过速率限制"
});
operation.Responses.TryAdd("500", new OpenApiResponse
{
Description = "服务器内部错误 — 请稍后重试或联系管理员"
});
}
}优点
缺点
缺点
总结
RESTful API 设计核心:资源用复数名词、HTTP 方法表达操作语义、状态码表示结果。统一响应格式包含 code/message/data。分页用 page/size/sort 参数,限制最大条数。错误响应包含字段级错误信息。全局异常处理中间件统一错误格式。URL 层级不超过3层,复杂查询用查询参数。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《RESTful API 设计规范》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《RESTful API 设计规范》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《RESTful API 设计规范》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《RESTful API 设计规范》最大的收益和代价分别是什么?
