RESTful API 设计规范
大约 9 分钟约 2773 字
RESTful API 设计规范
简介
REST(Representational State Transfer)是 Web API 最流行的架构风格。良好的 RESTful 设计让 API 直观、一致、易于使用。本文总结 RESTful API 的命名规范、HTTP 方法使用、状态码选择、版本管理和安全实践。
特点
URL 设计
资源命名
# 资源用复数名词
GET /api/users # 用户列表
GET /api/users/123 # 获取单个用户
POST /api/users # 创建用户
PUT /api/users/123 # 更新用户
DELETE /api/users/123 # 删除用户
# 子资源
GET /api/users/123/orders # 用户的订单列表
GET /api/users/123/orders/456 # 用户的某个订单
POST /api/users/123/orders # 为用户创建订单
# 嵌套不超过2层
# 差:/api/users/123/orders/456/items/789
# 好:/api/orders/456/items/789
# 过滤和排序(查询参数)
GET /api/users?status=active&page=1&pageSize=10
GET /api/orders?startDate=2026-01-01&endDate=2026-12-31
GET /api/products?sortBy=price&order=desc
# 搜索
GET /api/products/search?keyword=手机&category=电子URL 规则
| 规则 | 示例 | 说明 |
|---|---|---|
| 用名词不用动词 | /api/users | 动词由 HTTP 方法表达 |
| 复数形式 | /api/users | 统一用复数 |
| 小写字母 | /api/user-profiles | 短横线分隔 |
| 层级清晰 | /api/users/123/orders | 不超过2层嵌套 |
| 查询参数过滤 | ?status=active | 过滤用查询参数 |
HTTP 方法
CRUD 映射
| HTTP 方法 | 路径 | 说明 | 幂等 |
|---|---|---|---|
| GET | /api/users | 获取列表 | 是 |
| GET | /api/users/123 | 获取详情 | 是 |
| POST | /api/users | 创建 | 否 |
| PUT | /api/users/123 | 全量更新 | 是 |
| PATCH | /api/users/123 | 部分更新 | 否 |
| DELETE | /api/users/123 | 删除 | 是 |
.NET Controller 示例
/// <summary>
/// RESTful API Controller — 标准 CRUD
/// </summary>
[ApiController]
[Route("api/[controller]")]
[Produces("application/json")]
public class ProductsController : ControllerBase
{
private readonly IProductService _service;
public ProductsController(IProductService service)
{
_service = service;
}
/// <summary>
/// 获取产品列表(分页、过滤)
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<ProductDto>), 200)]
public async Task<IActionResult> GetAll(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 10,
[FromQuery] string? category = null,
[FromQuery] string? keyword = null,
[FromQuery] string sortBy = "createdAt",
[FromQuery] string order = "desc")
{
var result = await _service.GetPagedAsync(page, pageSize, category, keyword, sortBy, order);
return Ok(result);
}
/// <summary>
/// 获取单个产品
/// </summary>
[HttpGet("{id}")]
[ProducesResponseType(typeof(ProductDto), 200)]
[ProducesResponseType(404)]
public async Task<IActionResult> GetById(int id)
{
var product = await _service.GetByIdAsync(id);
return product != null ? Ok(product) : NotFound();
}
/// <summary>
/// 创建产品
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ProductDto), 201)]
[ProducesResponseType(400)]
public async Task<IActionResult> Create([FromBody] CreateProductRequest request)
{
var product = await _service.CreateAsync(request);
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
/// <summary>
/// 全量更新
/// </summary>
[HttpPut("{id}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> Update(int id, [FromBody] UpdateProductRequest request)
{
var success = await _service.UpdateAsync(id, request);
return success ? NoContent() : NotFound();
}
/// <summary>
/// 部分更新
/// </summary>
[HttpPatch("{id}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> Patch(int id, [FromBody] JsonPatchDocument<Product> patchDoc)
{
var success = await _service.PatchAsync(id, patchDoc);
return success ? NoContent() : NotFound();
}
/// <summary>
/// 删除
/// </summary>
[HttpDelete("{id}")]
[ProducesResponseType(204)]
[ProducesResponseType(404)]
public async Task<IActionResult> Delete(int id)
{
var success = await _service.DeleteAsync(id);
return success ? NoContent() : NotFound();
}
}HTTP 状态码
常用状态码
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 | OK | GET 成功、PUT/PATCH 更新成功 |
| 201 | Created | POST 创建成功 |
| 204 | No Content | DELETE 成功、PUT 无返回体 |
| 400 | Bad Request | 请求参数错误 |
| 401 | Unauthorized | 未认证 |
| 403 | Forbidden | 无权限 |
| 404 | Not Found | 资源不存在 |
| 409 | Conflict | 资源冲突(如重复创建) |
| 422 | Unprocessable Entity | 验证失败 |
| 429 | Too Many Requests | 限流 |
| 500 | Internal Server Error | 服务器异常 |
统一错误响应
/// <summary>
/// 统一错误响应格式
/// </summary>
public class ErrorResponse
{
public int Status { get; set; }
public string Message { get; set; } = "";
public string? Detail { get; set; }
public List<FieldError>? Errors { get; set; }
public string TraceId { get; set; } = Activity.Current?.Id ?? "";
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
}
public class FieldError
{
public string Field { get; set; } = "";
public string Message { get; set; } = "";
}分页规范
分页响应
/// <summary>
/// 分页响应
/// </summary>
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasPrevious => Page > 1;
public bool HasNext => Page < TotalPages;
}版本管理
URL 版本
// 方式1:URL 路径版本
[ApiController]
[Route("api/v1/[controller]")]
public class ProductsV1Controller : ControllerBase { }
[ApiController]
[Route("api/v2/[controller]")]
public class ProductsV2Controller : ControllerBase { }
// 方式2:HTTP Header
// X-API-Version: 2
// 方式3:查询参数
// /api/products?api-version=2
// .NET 内置版本控制
// builder.Services.AddApiVersioning(options =>
// {
// options.DefaultApiVersion = new ApiVersion(1, 0);
// options.AssumeDefaultVersionWhenUnspecified = true;
// options.ReportApiVersions = true;
// });API 版本管理进阶
// 使用 Microsoft.AspNetCore.Mvc.Versioning(NuGet)
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("X-API-Version"),
new QueryStringApiVersionReader("api-version"));
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// 带版本约束的 Controller
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetV1()
{
return Ok(new { version = "1.0", data = "basic product info" });
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetV2()
{
return Ok(new { version = "2.0", data = "enriched product info with reviews" });
}
[HttpDelete("{id}")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> DeleteV2(int id)
{
return NoContent();
}
}
// 版本废弃策略
// 1. 在响应头中标记:Sunset: Sat, 1 Jan 2027 00:00:00 GMT
// 2. 返回 Deprecation: true 头
// 3. 在 Link 头中指向新版本HATEOAS 与超媒体控制
超链接驱动 API
// HATEOAS(Hypermedia As The Engine Of Application State)
// 响应中包含可操作的链接,客户端不需要硬编码 URL
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public Links _links { get; set; } = new();
}
public class Links
{
public LinkInfo? Self { get; set; }
public LinkInfo? Next { get; set; }
public LinkInfo? Prev { get; set; }
public LinkInfo? First { get; set; }
public LinkInfo? Last { get; set; }
}
public class LinkInfo
{
public string Href { get; set; } = "";
public string Method { get; set; } = "GET";
public string Rel { get; set; } = "";
}
// 使用示例
[HttpGet]
public async Task<IActionResult> GetProducts([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var result = await _service.GetPagedAsync(page, pageSize);
var paged = new PagedResult<ProductDto>
{
Items = result.Items,
TotalCount = result.TotalCount,
Page = result.Page,
PageSize = result.PageSize,
_links = new Links
{
Self = new LinkInfo { Href = $"/api/products?page={page}&pageSize={pageSize}" },
Next = result.HasNext
? new LinkInfo { Href = $"/api/products?page={page + 1}&pageSize={pageSize}" }
: null,
Prev = result.HasPrevious
? new LinkInfo { Href = $"/api/products?page={page - 1}&pageSize={pageSize}" }
: null
}
};
return Ok(paged);
}批量操作
RESTful 批量 API 设计
// 批量创建
[HttpPost("batch")]
[ProducesResponseType(typeof(BatchResult<ProductDto>), 207)]
public async Task<IActionResult> BatchCreate([FromBody] List<CreateProductRequest> requests)
{
var results = new List<BatchItemResult<ProductDto>>();
foreach (var request in requests)
{
try
{
var product = await _service.CreateAsync(request);
results.Add(new BatchItemResult<ProductDto>(true, product));
}
catch (Exception ex)
{
results.Add(new BatchItemResult<ProductDto>(false, error: ex.Message));
}
}
var allSuccess = results.All(r => r.Success);
var anySuccess = results.Any(r => r.Success);
return StatusCode(
allSuccess ? 201 : anySuccess ? 207 : 400,
new BatchResult<ProductDto>(results));
}
// 批量删除
[HttpDelete("batch")]
public async Task<IActionResult> BatchDelete([FromQuery] int[] ids)
{
var deleted = 0;
var notFound = new List<int>();
foreach (var id in ids)
{
var success = await _service.DeleteAsync(id);
if (success) deleted++;
else notFound.Add(id);
}
return Ok(new { deleted, notFound });
}
// 批量查询
[HttpPost("batch/query")]
public async Task<IActionResult> BatchGet([FromBody] BatchQueryRequest request)
{
var products = await _service.GetByIdsAsync(request.Ids);
return Ok(products);
}
public record BatchQueryRequest(int[] Ids);
public record BatchResult<T>(List<BatchItemResult<T>> Items);
public record BatchItemResult<T>(bool Success, T? Data = default, string? Error = null);限流与安全
Rate Limiting 集成
// ASP.NET Core 7+ 内置限流
builder.Services.AddRateLimiter(options =>
{
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(context =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(),
factory: partition => new FixedWindowRateLimiterOptions
{
AutoReplenishment = true,
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
// 为特定端点配置独立限流
options.AddFixedWindowLimiter("fixed", options =>
{
options.PermitLimit = 10;
options.Window = TimeSpan.FromSeconds(10);
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 5;
});
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests;
if (context.Lease.TryGetMetadata(MetadataName.RetryAfter, out var retryAfter))
{
context.HttpContext.Response.Headers.RetryAfter = retryAfter.TotalSeconds.ToString();
}
await context.HttpContext.Response.WriteAsJsonAsync(new
{
error = "Too many requests",
retryAfter = retryAfter.TotalSeconds
}, cancellationToken);
};
});
app.UseRateLimiter();
// 在 Controller 上使用
[HttpGet("heavy")]
[EnableRateLimiting("fixed")]
public async Task<IActionResult> HeavyComputation()
{
return Ok(await _service.ComputeAsync());
}CORS 配置
// CORS — 跨域资源共享
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowFrontend", policy =>
{
policy.WithOrigins("https://app.example.com", "https://admin.example.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()
.SetPreflightMaxAge(TimeSpan.FromDays(1));
});
options.AddPolicy("AllowAll", policy =>
{
policy.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader();
});
});
app.UseCors("AllowFrontend");
// 安全建议:
// - 生产环境绝不能用 AllowAnyOrigin + AllowCredentials
// - 明确指定允许的域名
// - Preflight 请求设缓存,减少 OPTIONS 调用
// - 只暴露必要的 HTTP 方法和头内容协商
Accept 与 Content-Type
// ASP.NET Core 默认支持 JSON 内容协商
// 客户端通过 Accept 头指定期望的响应格式
// 添加 XML 格式支持
builder.Services.AddControllers(options =>
{
options.ReturnHttpNotAcceptable = true; // 不支持的格式返回 406
})
.AddXmlSerializerFormatters()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});
// 自定义格式化器
public class CsvOutputFormatter : TextOutputFormatter
{
public CsvOutputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("text/csv"));
SupportedEncodings.Add(Encoding.UTF8);
}
protected override bool CanWriteType(Type? type) => typeof(IEnumerable).IsAssignableFrom(type);
public override async Task WriteResponseBodyAsync(
OutputFormatterWriteContext context, Encoding selectedEncoding)
{
var response = context.HttpContext.Response;
var data = context.Object as IEnumerable;
// CSV 序列化逻辑
await response.WriteAsync("id,name,email\n", selectedEncoding);
}
}
// 注册自定义格式化器
builder.Services.AddControllers(options =>
{
options.OutputFormatters.Add(new CsvOutputFormatter());
});优点
缺点
总结
RESTful API 的核心原则:URL 代表资源,HTTP 方法代表操作,状态码表达结果。统一命名规范、分页格式、错误响应。GET 不修改数据,POST 创建,PUT 全量更新,PATCH 部分更新,DELETE 删除。生产环境务必做好版本管理和认证授权。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《RESTful API 设计规范》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《RESTful API 设计规范》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《RESTful API 设计规范》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《RESTful API 设计规范》最大的收益和代价分别是什么?
