RESTful API URI 设计的一些总结
非常赞的三篇文章:
本篇阅读目录:
- 1. HTTP Methods
- 2. JSON API 命名
- 3. API URI design
1. HTTP Methods
HTTP 常用方法:
- GET: 获取某个资源。
- POST: 创建一个新的资源。
- PUT: 替换某个已有的资源。
- PATCH: 修改某个已有的资源。
- DELETE:删除某个资源。
我原先以为修改某一个资源,也是用 POST,后来发现还有一个 PATCH,但发现 HttpClient 并没有提供此调用方法,需要我们进行扩展:
public static class HttpClientExtensions { public static async Task<HttpResponseMessage> PatchAsync(this HttpClient client, Uri requestUri, HttpContent iContent) { var method = new HttpMethod("PATCH"); var request = new HttpRequestMessage(method, requestUri) { Content = iContent }; HttpResponseMessage response = new HttpResponseMessage(); try { response = await client.SendAsync(request); } catch (TaskCanceledException e) { Debug.WriteLine("ERROR: " + e.ToString()); } return response; } }
调用代码:
HttpContent httpContent = new StringContent("Your JSON-String", Encoding.UTF8, "application/json"); var responseMessage = await httpClient.PatchAsync(new Uri("testUri"), httpContent);
2. JSON API 命名
相关阅读: You should use camelCase with JSON, but snake_case is 20% easier to read
camelCase(骆驼命名)我们都非常熟悉,因为 C# 就是使用的这个命名法,snake_case(蛇形命名)适用于 python 和 ruby,比如商品 ID,camelCase 会命名为 productId,snake_case 则会命名为 product_id。
需要注意的是,snake_case 只限于 JSON API 命名,并不限于 URI,URI 中一般也不会使用下划线,为什么要对 JSON API 进行规范命名?因为 RESTful 是无状态风格,也就是说 RESTful API 并不限于某一种客户端进行调用,所以 JSON API 的命名必须要规范,如果只是 C# 调用的话,那么命名采用 camelCase 命名就可以了,但显然并不是这样,最后得出的结论是使用 snake_case 命名会比较好,以后在设计的时候,需要注意了。
3. API URI design
API URI 设计最重要的一个原则: nouns (not verbs!) ,名词(而不是动词)。
CRUD 简单 URI:
- GET /users- 获取用户列表
- GET /users/1- 获取 Id 为 1 的用户
- POST /users- 创建一个用户
- PUT /users/1- 替换 Id 为 1 的用户
- PATCH /users/1- 修改 Id 为 1 的用户
- DELETE /users/1- 删除 Id 为 1 的用户
上面是对某一种资源进行操作的 URI,那如果是有关联的资源,或者称为级联的资源,该如何设计 URI 呢?比如某一用户下的产品:
- GET /users/1/products- 获取 Id 为 1 用户下的产品列表
- GET /users/1/products/2- 获取 Id 为 1 用户下 Id 为 2 的产品
- POST /users1/products- 在 Id 为 1 用户下,创建一个产品
- PUT /users/1/products/2- 在 Id 为 1 用户下,替换 Id 为 2 的产品
- PATCH /users/1/products.2- 修改 Id 为 1 的用户下 Id 为 2 的产品
- DELETE /users/1/products/2- 删除 Id 为 1 的用户下 Id 为 2 的产品
还有一种情况,我们一般在设计 API 的时候,会进行一些查询操作,比如分页和排序等,API 方法参数设计可能很容易,那重要的 URI 该如何设计呢?我们先看这样的一个设计:
[HttpGet] [Route("api/wzlinks/users-{spaceUserId}/{pageIndex=1}/{pageSize=20}")] public async Task<IEnumerable<WzLinkDTO>> GetPagedList(int spaceUserId, int pageIndex, int pageSize) { ..... }
首先,这个 URI 想要表示的意思是:获取某一用户下,分页查询的网摘列表,这个 API 设计好不好呢?我们看下 GitHub 中的一个 API:
"current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}"
差别是不是很大?而且 URI 表达的也必要混乱,查询应该是参数,并且是对 URI 进行的查询,所以放在 URI 中会不太合适,我们完善下:
[HttpGet] [Route("api/users/{space_user_id}/wzlinks/{page=1}/{per_page=20}")] public async Task<IEnumerable<WzLinkDTO>> GetPagedList(int spaceUserId, int page, int per_page) { ..... }
URI 表达为:获取 space_user_id 为 1 用户下的网摘分页列表,上面设计会不会更好些呢?调用示例:
api.cnblogs.com/api/users/1/wzlinks?page=1&per_page=20
GitHub API(规范参考): https://api.github.com
{ "current_user_url": "https://api.github.com/user", "current_user_authorizations_html_url": "https://github.com/settings/connections/applications{/client_id}", "authorizations_url": "https://api.github.com/authorizations", "code_search_url": "https://api.github.com/search/code?q={query}{&page,per_page,sort,order}", "emails_url": "https://api.github.com/user/emails", "emojis_url": "https://api.github.com/emojis", "events_url": "https://api.github.com/events", "feeds_url": "https://api.github.com/feeds", "following_url": "https://api.github.com/user/following{/target}", "gists_url": "https://api.github.com/gists{/gist_id}", "hub_url": "https://api.github.com/hub", "issue_search_url": "https://api.github.com/search/issues?q={query}{&page,per_page,sort,order}", "issues_url": "https://api.github.com/issues", "keys_url": "https://api.github.com/user/keys", "notifications_url": "https://api.github.com/notifications", "organization_repositories_url": "https://api.github.com/orgs/{org}/repos{?type,page,per_page,sort}", "organization_url": "https://api.github.com/orgs/{org}", "public_gists_url": "https://api.github.com/gists/public", "rate_limit_url": "https://api.github.com/rate_limit", "repository_url": "https://api.github.com/repos/{owner}/{repo}", "repository_search_url": "https://api.github.com/search/repositories?q={query}{&page,per_page,sort,order}", "current_user_repositories_url": "https://api.github.com/user/repos{?type,page,per_page,sort}", "starred_url": "https://api.github.com/user/starred{/owner}{/repo}", "starred_gists_url": "https://api.github.com/gists/starred", "team_url": "https://api.github.com/teams", "user_url": "https://api.github.com/users/{user}", "user_organizations_url": "https://api.github.com/user/orgs", "user_repositories_url": "https://api.github.com/users/{user}/repos{?type,page,per_page,sort}", "user_search_url": "https://api.github.com/search/users?q={query}{&page,per_page,sort,order}" }