前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >也许你并不需要 REST

也许你并不需要 REST

作者头像
李海彬
发布2018-07-26 09:50:32
4010
发布2018-07-26 09:50:32
举报
文章被收录于专栏:Golang语言社区Golang语言社区

You Don't Need REST

Nearly 10 years ago, Leonard Richardson and Sam Ruby publishedRESTful Web Services. I remember eagerly waiting for that book before it came out. Today, REST is still regarded as the state-of-the-art API architecture. It's not hard to see its benefit in comparison to preceding RPC solutions of that time. RESTful APIs make sense, because of the obvious HTTP verb mapping to CRUD methods, and the ease at which individual resources may be addressed and cached in HTTP middleware.

When you're implementing client software to talk to RESTful APIs though, naturally the HTTP gets abstracted: GET /resource/id becomesresource.get(id) and POST /resource becomes resource.create(). The vast majority of applications I touched in the past two years weren't so data intensive to warrant even thinking about HTTP caching strategies. And if there were one or two endpoints that did require caching, a localized implementation worked best.

So having RESTfulness as a design constraint, in this case, is justunnecessary complexity. In order to streamline our JavaScript client's calls to our core RESTful API at STORED e-commerce, we created a gateway that maps certain URLs to method calls, and let the code inside those methods peform the actual RESTful requests to our core API. First we added it directly to our Nuxt stack as a Koa middleware:

代码语言:javascript
复制
 1app.use(async (ctx, next) => {
 2  if (!ctx.path.startsWith('/api') || ctx.request.method !== 'POST') {
 3    await next()
 4  } else {
 5    const apiMethod = ctx.path.split('/api/')[1]
 6    let [resource, method] = apiMethod.split('/')
 7    method = translatePathToMethod(method)
 8    const request = { payload: ctx.json.payload }
 9    const response = await api[resource][method](request)
10    ...
11  }

translatePathToMethod() translates an HTTP request like/api/resource/method to a api.resource.method() JavaScript call on the server. Our actual code does quite a bit more, such as checking and refreshing auth tokens on-the-fly, but you get the idea.

The takeaway here is that if you're writing a RPC-like proxy to perform RESTful API calls that do not require any caching, you might as well remove the extra call and place all your code in that RPC method and basically have a JSON-pure API.

David Gilbertson, Michael S. Mikowski and Thomas Jetzinger havewritten similar pieces about REST's unnecessary complexity.

An API gateway in Go

Our Koa-based API proxy gets the job done, but looking forward we wanted to turn this into a fast and reliable piece of our toolset. AtSTORED e-commerce we already do a lot of Python, with our core RESTful API written entirely in it. But we have had an infatuation with Go that's been growing over the years, and decided to give it a try.

For references we looked at many HTTP client implementations in Go but eventually settled on go-github, written by Google developers. go-github offers an excellent starting point with carefully crafted yet minimalnet/http package abstractions. Each set of methods associated with a resource is kept in a separate file (such as activity.go), with all key interfaces and methods defined in the main github.go file.

The problem is that we need to infer what resource and method are being called from the request URI. Not a trivial task in Go. I started with the main request handler, using gorilla's mux as my routing library, and parsing the necessary parts to make the method call:

代码语言:javascript
复制
 1func APIGateway(w http.ResponseWriter, request *http.Request) {
 2  body, err := ioutil.ReadAll(request.Body)
 3  if err != nil {
 4    http.Error(w, err.Error(), http.StatusInternalServerError)
 5  }
 6  apiCall := new(APICall)
 7  if err := json.Unmarshal(body, &apiCall.Payload); err != nil {
 8    http.Error(w, err.Error(), http.StatusInternalServerError)
 9  }
10  methodParts := strings.Split(mux.Vars(request)["method"], "/")
11  resource := strings.Title(methodParts[0])
12  method := translatePathToMethod(methodParts[1])
13  apiClient := NewClient()
14  data, _, err := apiClient.CallMethodByName(resource, method, apiCall.Payload)
15  parsedData, err := json.Marshal(&data) 
16  if err != nil {
17    http.Error(w, err.Error(), http.StatusInternalServerError)
18  }
19  w.Header().Set("Content-Type", "application/json")
20  log.Println(string(parsedData))
21  io.WriteString(w, string(parsedData))
22}
23func main() {
24  router := mux.NewRouter().StrictSlash(true)
25  router.HandleFunc("/api/{method:.*}", APIGateway)
26  log.Println("Running API gateway at port 4000")
27  log.Fatal(http.ListenAndServe(":4000", router))
28}

Several helper functions and type definitions have been omitted for brevity. The most challenging part is of course CallMethodByName(). This did take a lengthy research but thanks to this StackOverflow threadand subsequent reading of The Laws of Reflection, I was able to put it together below. The cool thing about Go's reflect package is that it can give access to nearly everything in the language, making it as malleable as JavaScript if you manage to wrap your head around it.

代码语言:javascript
复制
 1func (c *Client) CallMethodByName(
 2  resource string,
 3  method string,
 4  payload json.RawMessage,
 5) (
 6  *json.RawMessage,
 7  *Response,
 8  error,
 9) {
10  resourceObj, err := resourceByName(c, resource)
11  if err != nil {
12    log.Fatal(err)
13  }
14  methodFunc, err := methodByName(resourceObj, method)
15  if err != nil {
16    log.Fatal(err)
17  }
18  in := []reflect.Value{
19    reflect.ValueOf(context.Background()),
20    reflect.ValueOf(payload),
21  }
22  results := methodFunc.Call(in)
23  data := results[0].Interface().(*json.RawMessage)
24  response := results[1].Interface().(*Response)
25  return data, response, nil
26}

As you can see, values are passed to the final method in the form of areflect.Value slice. Return values are then cast back to their expected types before returning. Our code is still evolving, and there are of course several potential errors that need to be addressed, but it now successfully translates resource/get-something toResource.GetSomething() and all you have to do is add the service definitions and methods you'll use.

A working boilerplate is available at github.com/stored/pathway. Pull requests are very much welcome.

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-05-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Golang语言社区 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • You Don't Need REST
    • An API gateway in Go
    相关产品与服务
    Serverless HTTP 服务
    Serverless HTTP 服务基于腾讯云 API 网关 和 Web Cloud Function(以下简称“Web Function”)建站云函数(云函数的一种类型)的产品能力,可以支持各种类型的 HTTP 服务开发,实现了 Serverless 与 Web 服务最优雅的结合。用户可以快速构建 Web 原生框架,把本地的 Express、Koa、Nextjs、Nuxtjs 等框架项目快速迁移到云端,同时也支持 Wordpress、Discuz Q 等现有应用模版一键快速创建。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档