如何设计更好的WebAPI

随着移动互联网和Web开发技术的发展,在项目中需要为越来越多的跨平台应用提供统一化的API接口。那么作为一个后端开发者,如何设计并开发出更规范、更清晰、更好用的WebAPI呢?


1.如何理解API?

宽泛的讲,API(Application Programming Interface)指的是应用程序编程接口,用来提供预先定义好的方法和函数,使用者无需去关心其内部细节就能实现相应的功能。而现在我们提到API,脑海中首先想到的往往是基于HTTP协议传输的WebService技术。
从概念上讲,API与开发语言无关,理论上具备网络编程功能的编程语言,如Java、C#、PHP、Node等,都能够通过相应HTTP请求并构造HTTP包来实现Web API,虽然它们内部的实现原理不同,但确确实实的提供了API服务。
对于.NET平台的开发者来讲,使用C#编程语言有WCF、WebService、ASP.NET WebAPI等多种技术方案。由于ASP.NET WebAPI简单轻量的特点,使其受到越来越多人的欢迎。

2.什么是好的WebAPI?

从我个人的角度理解,暂且不论是什么编程语言,WebAPI最终是要提供给第三方使用的,可能是你的同事或者合作伙伴,所以后端开发人员提供的WebAPI可以看作是个人或者团队的产品输出。从产品的特质来看,WebAPI首先要能满足使用要求,其次要具备标准化、稳定性等特征。如果能在这些特征基础上为使用者提供更多额外的功能,那才是好的WebAPI。
举个通俗的例子,我们在生活中都会用到插线板,不管哪个品牌的插线板都遵循同样的标准。如果在*提供不了220V标准电压,废品;如果电器插座插不进插孔,废品;如果使用一小时寿命就终结,废品…只有在提供安全、稳定、标准供电的基础上再去考虑彩色设计、卡通造型、防水保护、耗能统计这些附加功能,才能说是一个好的插线板。
同样现在很多WebAPI只是解决了能用的问题,但是否好用还要细细思量。来看一个我们之前的WebAPI示例。

Method:GET
URL:api/TestResults/GetView_ResultList?pagesize={pagesize}&currentpage={currentpage}&where={where}&orderby={orderby}
Request:
Response:{ “sample string 1”: “sample string 2”,“sample string 3”: “sample string 4”}

很多前端开发人员看到这个API接口,心里肯定会想WTF?但遗憾的是,当我们刚开始接触WebAPI时,大量的接口都是这种形式的,现在回头来看真的是很糟糕。没有文档、URL复杂、没有返回状态,除了开发者本人没人知道这个接口要干嘛。幸运的是随着对WebAPI的深入了解,我们开始认识到设计一套清晰易用的WebAPI是多么重用,并开始将WebAPI的设计向一些成熟的规范和约定靠拢,其中之一就RESTful风格的API设计。

3.RESTful风格的API设计

REST是“REpresentational State Transfer”的缩写,可以翻译成“表现状态转换”,理解起来比较抽象。REST是一种架构风格,并不是一个技术框架也非强制规范,而是通过对资源的定位、操作藉由HTTP协议提供服务,其中资源可以是数据、图片、流程等具体的或抽象的对象。简单来说就是通过定义URI和HTTP状态码,让API更加简洁清晰、易懂易记、富有层次。通过RESTful风格的API可以解决我们在WebAPI开发过程中遇到的一些问题。

3.1.如何让 URI 清晰易懂?

好的代码无需注释,代码即注释。对于API也一样,好的URI设计无需文档就能理解用途。在API接口的URL中应仅使用名词而不是动词。此外所有名词小写,同时注意复数与单数的使用差异。

/orders             所有订单
/orders/12          具有指定标识的订单
/orders?sort=asc    按照升序排列订单

3.2.如何最大程度地利用 HTTP 协议?

合理利用HTTP协议本身的方法GET、POST、PUT、DELETE。如果说上面的URI对资源进行定位,那么HTTP方法就是对资源进行操作,几个典型的用法有:

HTTP方法 用途 示例
GET 获取数据集合 /orders
GET 获取指定数据 /orders/{id}
POST 新建数据 /orders
PUT 完整更新数据 /orders/{id}
PATCH 部分更新数据 /orders/{id}
DELETE 删除数据 /orders/{id}

3.3.如何选择数据格式?

使用JSON或XML传输数据,其中JSON阅读性更高,且更易扩展,能够使用多种环境和编程语言。

{
    status:200,
    data:[
    		{"id":1,"name":"订单1"},
    		{"id":2,"name":"订单2"},
    		{"id":3,"name":"订单3"}
    	],
    msg:""
}

3.4.如何通过状态码表示出错信息?

充分使用HTTP状态码。我们知道HTTP协议是Request-Response模式的,针对各种情况的HTTP请求,服务器应该反馈恰当的状态码。如果我们把错误信息放在状态码为200的反馈中,势必会造成混乱。所以完全可以利用状态码将HTTP自身的错误和服务器内部的错误区分开。

状态码 含义 用途
200 OK 表示请求成功,例如GET请求或更新、删除资源成功。
201 Created 表示POST创建资源成功。
204 No Content 表示请求已处理成功,但未返回任何内容。
400 Bad Request 表示服务器无法理解客户端的请求内容。
401 Unauthorized 表示不允许客户端访问资源,需要客户端自查传入的参数是否满足条件。
403 Forbidden 表示请求有效且客户端通过身份认证,但不允许客户端访问资源。
404 Not Found 表示请求的资源不存在。
415 Unsupported Media Type 表示服务器媒体类型Content-Type或Accept出现错误。
500 Internal Server Error 表示服务器内部错误
503 Service Unavailable 表示服务器已关闭或无法接受、处理请求。

3.5.如何规划 API 的版本?

在URI中加入版本信息,可以通过仅仅修改版本号就实现API服务的迁移,而无需修改所有API接口的地址。

/v1/orders
/v2/orders

3.6.如何通过查询参数实现搜索分页?

/v1/orders?sort=date     按照条件排序
/v1/orders?page=4        按照页码分页
/v1/orders?filter=xxx    按照条件过滤  

3.7.如何定义返回数据结构?

用结构化的JSON数据填充HTTP Response的Body。参考JSON API规范如何传递数据的格式。在*结点使用data、errors、meta来描述数据、错误信息和元信息,其中data和errors不能在同一个返回数据中出现,meta一般情况下可以不用。

JSON API文档

示例
{
  "meta": {
    "version":"2.1",
    "copyright": "Copyright 2015 Example Corp.",
    "authors": [
      "Yehuda Katz",
      "Steve Klabnik"
    ]
  },
  "data": {
    // 数据主体
  }
}
data
{
  "data": {
    "type": "orders",
    "id": "1",
    "attributes": {
      // 资源的具体数据
    },
    "relationships": {
      // 资源的可选属性,如关联数据、资源地址等
    }
  }
}

其中type描述数据的类型,可以对应为数据模型的类名;id为资源的唯一标识。

errors
{
	"errors":[
		{
			"code":10001,
			"title":"orderNo is null"
		},
		{
			"code":10002,
			"title":"the result can't be null",
			"detail":""
		}
	]
}

其中code为具体的错误码,title为错误信息,在开发测试环境下可以添加detail字段用来放置更详细的错误信息。

4.如何在ASP.NET WebAPI实现RESTful风格?

4.1.基本配置
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/v1/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
4.2.API控制器代码
    public class ClientController : ApiController
    {
        private DBEntities db = new DBEntities();
        /// <summary>
        /// 获取全部资源
        /// </summary>
        /// <returns></returns>
        public IQueryable<SYS_Client> GetClients()
        {
            return db.SYS_Client;
        }

        /// <summary>
        /// 获取指定标识的资源
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [ResponseType(typeof(SYS_Client))]
        public async Task<IHttpActionResult> GetClientById(string id)
        {
            SYS_Client client = await db.SYS_Client.FindAsync(id);
            if (client == null)
            {
                return NotFound();
            }

            return Ok(client);
        }

        /// <summary>
        /// 更新资源
        /// </summary>
        /// <param name="id"></param>
        /// <param name="client"></param>
        /// <returns></returns>
        [ResponseType(typeof(void))]
        public async Task<IHttpActionResult> PutSYS_Client(string id, SYS_Client client)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            if (id != client.RowGuid)
            {
                return BadRequest();
            }

            db.Entry(client).State = EntityState.Modified;

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!IsExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return StatusCode(HttpStatusCode.NoContent);
        }

        /// <summary>
        /// 新增资源
        /// </summary>
        /// <param name="client"></param>
        /// <returns></returns>
        [ResponseType(typeof(SYS_Client))]
        public async Task<IHttpActionResult> PostSYS_Client(SYS_Client client)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }

            db.SYS_Client.Add(client);

            try
            {
                await db.SaveChangesAsync();
            }
            catch (DbUpdateException)
            {
                if (IsExists(client.RowGuid))
                {
                    return Conflict();
                }
                else
                {
                    throw;
                }
            }

            return CreatedAtRoute("DefaultApi", new { id = client.RowGuid }, client);
        }

        /// <summary>
        /// 删除资源
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        [ResponseType(typeof(SYS_Client))]
        public async Task<IHttpActionResult> DeleteSYS_Client(string id)
        {
            SYS_Client client = await db.SYS_Client.FindAsync(id);
            if (client == null)
            {
                return NotFound();
            }

            db.SYS_Client.Remove(client);
            await db.SaveChangesAsync();

            return Ok(client);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                db.Dispose();
            }
            base.Dispose(disposing);
        }

        private bool IsExists(string id)
        {
            return db.SYS_Client.Count(e => e.RowGuid == id) > 0;
        }
    }

4.3.API清单

如何设计更好的WebAPI


以上内容只是抛砖引玉,如果想要设计和开发更好用的WebAPI,无疑要经过更多的改进和优化。只有不断进步,才能输出更好的产品。