go-restful api design

I have been using JAX-RS for many REST-based service implementations in Java. As part of my journey into the Google Go programming language, I am exploring designs for such REST support using the standard net/http package in Go.

JAX-RS provides a set of Annotation classes which can be used to add meta-data to classes, methods and method arguments. In JAX-RS these annotation are used to specify the mapping between a Http Request to a Java method and its arguments.

With the exception of tags (string) for struct fields, there is no such concept in Go (AFAIK). So, I tried several approaches to specify this mapping information when defining a REST Resource. For that I wrote a simple example that defines a UserResource which has the REST versions of the CRUD operations on a domain type “User”.

Iteration 1: ResourceMethod and ResourceMethodContainer

The first part below shows the implementation of a UserResource with functions for each GET,PUT,POST and DELETE. The second part shows the Go code to specify the mappings for each function.

type UserResource struct {
    restful.ResourceMethodContainer
}
func (self UserResource) GetUser(userid restful.PathParam) restful.Response {
    response := restful.Response{Status: http.StatusOK}
    // someUser := fetch by userid
    // response.SetEntity(someUser)
    response.AddHeader(restful.HeaderLastModified, time.Now().Add(time.Duration(1000)))
    return response
}
func (self UserResource) UpdateUser(userid restful.PathParam, who User) restful.Response {
    // update User with id = userid
    return restful.Response{Status: http.StatusOK}
}
func (self UserResource) CreateUser(userid restful.PathParam, who User) restful.Response {
    // new User with id = userid
    return restful.Response{Status: http.StatusCreated}
}
func (self UserResource) DeleteUser(userid restful.PathParam) restful.Response {
    // delete User where id = userid
    return restful.Response{Status: http.StatusOK}
}

The functions in UserResource all return a restful.Response which is used to specifiy the status, the response body (bytes) and optional Http headers.

A mapping is specified with a ResourceMethod struct ; its Function field must match to a function defined in the UserResource package. That is the reason why the UserResource has an anonymous field ResourceMethodContainer.

// Create a new UserResource and register all mappings between HTTP requests and UserResource functions
func New() UserResource {
    res := UserResource{restful.ResourceMethodContainer{Root: "/"}}
    userPath := "/users/{user-id}"
    res.Register(restful.ResourceMethod{
        Method:   restful.GET,
        Path:     userPath,
        Produces: restful.XML,
        Function: "GetUser"})

    res.Register(restful.ResourceMethod{
        Method:   restful.POST,
        Path:     userPath,
        Consumes: restful.XML,
        Function: "UpdateUser"})

    res.Register(restful.ResourceMethod{
        Method:   restful.PUT,
        Path:     userPath,
        Consumes: restful.XML,
        Function: "CreateUser"})
    res.Register(restful.ResourceMethod{
        Method:   restful.DELETE,
        Path:     userPath,
        Function: "DeleteUser"})
    return res
}

Iteration 2: Apply Builder pattern to reduce clutter

The idea here is to use a ResourceMethodBuilder to build ResourceMethod structs using a fluent api. The builder is initialized with defaults and has methods to change its state in order to build a ResourceMethod. For example, the call GET("GetUser") tells the builder to change the current Http method to "GET" and that it is mapped to the function "GetUser".
func New() *UserResource {
    res := new(UserResource)
    res.Path("/users")

    builder := restful.ResourceMethodBuilder {
       Produces: restful.XML,
       Consumes: restful.XML }
    builder.Path("/{user-id}")

    res.Add(builder.GET("GetUser").Build())
    res.Add(builder.POST("UpdateUser").Build())
    res.Add(builder.PUT("CreateUser").Build())
    res.Add(builder.DELETE("DeleteUser").Build())

    res.Add(builder.GET("FindUsersByName").Path("/").QueryParam("name").Build())

    return res
}
Below, the restful.Response is replaced by a function parameter of type restful.ResponseBuilder. It has convenience build functions to compose the response. Its implementation will use the http.ResponseWriter for actually writing the response content including the marshalled entity.
func (self UserResource) FindUsersByName(response restful.ResponseBuilder, name restful.QueryParam) {
    users := []User{} // select from User where name  = ?
    response.StatusOK()
    response.AddHeader(restful.HeaderLastModified, time.Now().Add(time.Duration(1000)))
    response.Entity(users)
}

Iteration 3: Introduce defaults on the Resource and override per method if needed

func New() *UserResource {
    resource := new(UserResource)
    // set the root path and defaults for accept and contentType
    resource.Path("/users").Accept(restful.XML).ContentType(restful.XML)

    // create a builder for Resource Methods
    builder := resource.NewMethodBuilder().Path("/{user-id}")

    // add methods using current path, contentType and accept
    resource.Add(builder.GET("GetUser"))
    resource.Add(builder.POST("UpdateUser"))
    resource.Add(builder.PUT("CreateUser"))
    resource.Add(builder.DELETE("DeleteUser"))

    // change current contentType and path for function FindUsersByName
    builder.ContentType(restful.JSON).Path("/")
    resource.Add(builder.GET("FindUsersByName").QueryParam("name"))

    return resource
}

Iteration 4:  Binding Routes to Functions

In this iteration, I decided to change the name of ResourceMethodContainer to WebService. The combination "restful.WebService" is clear about what its functionality will provide. The next (and biggest) change is to use Functions as first class citizens. Instead of storing the function name in the Resource and resolving this to a function at runtime, I introduced Route structs that are initialized with a reference to the function. In the example below, a Route is created that binds a GET on "/users/{user-id}" to the function GetUser. Finally, I simplified the signature for functions that are used for mapping (Routes) from Http requests. A restful.Request is used to access Path,Query,URI and Header information. A restful.Response is used to write back the response composed of Status,Header and Body (bytes). In previous api designs, I tried to use an explicit argument mapping strategy i.e. the parameter value "{user-id}" could have been mapped to the "userid PathParam". This complicates the implementation of go-restful but also causes challenges for functions that need more information from the Request or need to add more information to the Response.
type UserService struct {
    restful.WebService
}
func New() *UserService {
    service := new(UserService)
    service.Path("/users").Accept("application/xml").ContentType("application/xml")
    
    service.Route(service.Method("GET").Path("/{user-id}").To(GetUser)) 
    return service
}

func GetUser(request *restful.Request, response *restful.Response) {
    id := request.PathParameter("user-id")
    wantsDetails := request.QueryParameter("details") 
    x := request.Header.Get("X-Something")
    // ... fetching the user    
    response.StatusOK()
    response.AddHeader("Last-Modified", time.Now().Add(time.Duration(1000)).String())
    response.Entity(user)
}

Iteration 5 (2012-12-04): Document the API

To allow for a more detailed auto-generated API document, the RouteBuilder (and therefore Route) is extended to include comment Doc(string), the request payload type Reads(…) and the response payload type Write(…).

  service.Route(service.GET("?from={from}&to={to}&type={type}&center={center}").
        Doc(`Get all (filtered) connections for all applications and the given scope`).
        To(getFilteredConnections).
        Writes(model.Connection{}))
        
    service.Route(service.PUT("/from/{from}/to/{to}/type/{type}?allowCreate={true|false}").
        Doc(`Create a new connection using the from,to,type values`).
        PathParam("from", "comma separated list of application ids").
        PathParam("to", "comma separated list of application ids").
        PathParam("type", "comma separated list of application ids").
        QueryParam("allowCreate", "if true then create any missing applications").
        To(putConnection).
        Reads(model.Connection{}))

Update 1: Others designs (and implementations) are rest2go and gorest

Update 2: go-restful is now work-in-progress

comments powered by Disqus