A simple REST framework for Go(#golang)
By: Aditya Bhasin
Tags: Libraries, Framework, Rest Api, Golang

Build your own

Go encourages to build your own abstractions and frameworks customised to your needs. With this in mind, we can use the standard HTTP package to build a simple framework for REST APIs

http.Handler

At the heart of the http package in Go is the Handler interface. A simple rule, if you need to build your own framework or handle requests, your struct must implement this interface.

Now that we know what is the bare minimum we need to implement, lets gather our requirments

As a REST framework, we need to provide the following bare minimum features

Ability to register end points Ability to restrict HTTP Methods on the end points Ability to parameterise path parameters (example: /mystore/books/{book_id}) Optional: Simple error handling/standard http response handler that decorates headers, marshals requests, etc

Route

We can consider each end point as a route to access resource.

At the core of it the route has the following attributes

A request end point (/mystore/books/{book_id}) Allowed HTTP methods — Get, Post, Delete, etc http.Handler — the handler to delegate the request for business logic

Parameterised path parameters — parameterised path parameters can be easily defined using standard regex patterns. Once again the standard Go library provides a robust regex package that we can leverage on.

thus /mystore/books/{book_id} becomes /mystore/books/(?P<book_id>\\d+)

We now can compile the regex matcher at the start of our application and reuse the matcher for each request(more on this later)

We can now model our route struct

type route struct {

endpoint string // /mystore/books/(?P<book_id>\\d+

matcher *regexp.Regexp // compiled regex of the endpoint

handler http.Handler // the handler to invoke on match

allowedMethods []string }

Router

Now that we have defined the basic building block of each end we can build a higher level struct that can be used to manage a collection of these routes

type router struct { routes map[string]*route // manages a collection of routes

}

We can provide a convenient method for clients to register routes

func (r *router) Handle(apiName, path string, handler http.Handler, methods ...string) error { // parse the path and compile a regex pattern

matcher, err := regexp.Compile(path)

if err != nil {

return err

}

rr.routes[apiName] = &route{path, matcher, handler, methods}

return nil

}

Registering a listener.

We can create a new router in our application and pass the router instance to http.ListenAndServe

router := new(router) router.Handle("mybooks", "/mystore/books/(?P<book_id>\\d+", my_book_handler, "GET" ) // assume we only handle GET. my_book_handler is a function that adheres to http.Handler spec err := http.ListenAndServe(":8080", router)

ServeHTTP

Our router implements the http.Handler interface. This is the main handler we shall register to out http Listener.

The ServeHTTP method receives the request, loops thru each route and if the request path matches invokes the route handler

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {

requesPath := req.URL.EscapedPath()

for _, v := range r.routes {

if params,OK := v.match(requestPath) {

if v.allowed(req.Method) {

v.handler.ServeHTTP(w, req)

return // we are done processing this request

} else {

// return a 405 - method not allowed

w.WriteHeader(405)

return

}

}

// we did not find a matching route. Return a 404

w.WriteHeader(404)

return

}

AllowedMethods

We can provide a convenient method on each route to filter out if the request method is allowed

func (r *route) allowed(method string) bool {

if r.methods != nil {

for _, m := range r.methods {

if m == method {

return true

}

}

return false // no match

}

return true // we allow all methods

}

Path Parameters

We can easily extract the path parameters of our request when doing a match on the route.

func (r *route) match(path string) (map[string]string, bool) {

matches := r.matcher.FindStringSubmatch(path)

if matches != nil && len(matches) > 0 {

// matched path. Return the path params and true

var params = make(map[string]string)

for i, name := range r.matcher.SubexpNames() {

params[i] = matches[i]

}

return params, true

}

// no match return false

return nil, false

}

we can then add the path params in the request context in our ServeHTTP method

func (r *router) ServeHTTP(w http.ResponseWriter, req *http.Request) {

requesPath := req.URL.EscapedPath()

for _, v := range r.routes {

if params,OK := v.match(requestPath) {

if v.allowed(req.Method) {

ctx := request.Context()

newCtx := context.WithValue(ctx, "PathParams", params)

rCon := req.WithContext(newCtx) v.handler.ServeHTTP(w, rCon)

return // we are done processing this request

} else {

// return a 405 - method not allowed

w.WriteHeader(405)

return

}

}

// we did not find a matching route. Return a 404

w.WriteHeader(404)

return

}

Now our handlers with business logic can do a simple “req.Context().Value(“PathParams”) and process the path params