From Ruby's Grape to Martini in Go for Building Web API Server
We have a website that is built by Rails. We also serve a bunch of APIs for customers. The api server is built on Grape, an amazing REST-like API micro-framework for Ruby. Recently I uses my spare time to learn Golang, a pretty new but fast growing language, and is extremely impressed by it’s simplicity and efficiency. I prefer learn by doing, so I start this experiment to try to re-write our api using Go, I want to see how hard it is to write code in Go comparing to Ruby.
I googled around to try to find a web framework in Golang, that is suitable for building an api service, and also easy to start with. And I finally found Martini. I really like it’s simple design of routing, flexible middleware handlers, and smart Injector. In the following paragraph, I’m going to compare Grape to Martini on how to code a basic version of our api server. The complete source code can be found on github: https://github.com/steventen/grape-vs-martini.
Example Requirements
The api server just uses a key
param in the query string for authentication. And it responses with a customized json format, like this
{"status": "Success", "data": [...]} # if success
{"status": "Fail", "error_message": "Bad api key"} # if failed
To simplify this experiment, we only define two models: Company and Project. A Company has many projects, and each project belongs to a company. Every company includes a unique api key for authentication. This example only implements two API endpoints:
GET /projects(.json)
GET /project/:id(.json)
Let’s see how we can implement this example on each web framework.
Implementation On Grape
Models
Our grape server is mounted on Rails. In Rails’s ActiveRecord, those two models can be represented as
class Project < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :projects
end
We also use grape-entity gem to manage model’s fields to be exposed, and alias names to be shown in json response.
module MySite
module APIEntities
class Project < Grape::Entity
expose :id
expose :name
end
end
end
Authentication
Authentication by api key needs to be done before every request, in Grape, you just need to put authentication method into before
block, you can always define Helpers
to make your code clean:
class API < Grape::API
# ... ...
helpers do
def current_company
key = params[:key]
@current_company ||= Company.where(:api => key).first
end
def authenticate!
error!({ "status" => "Fail", "error_message" => "Bad Key" }, 401) unless current_company
end
end
before do
authenticate!
end
# ... ...
end
Routes
Grape has very convenient get
, post
methods, our api routes can be coded like:
class API < Grape::API
# ... ...
get "projects" do
projects = current_company.projects
present :data, projects, :with => APIEntities::Project
present :status, "Success"
end
get "projects/:id" do
project = current_company.projects.where(id: params[:id]).first
if project
{"data" => {"id" => project.id, "name" => project.name}, "status" => "Success"}
else
# error!({ "status" => "Fail", "error_message" => "Failed to save project" }, 404)
{ "status" => "Fail", "error_message" => "Failed to save project" }
end
end
end
Note that Grape also provides some great helper methods like namespace
, route_param
for you to easily create complex routes, and methods like params
to do parameter validations. Those features are not shown in this example.
With Rails’ Active Record query interface, this code is fairly simple. In github repo, there are two versions, one is built directly on pure Rack, you can view the source code here. Another version is on Rails, source code is here.
Implementation On Martini
Models
Models can be represented by struct
in golang:
type Project struct {
Id int `json:"id"`
Name string `json:"name"`
}
type Company struct {
Id int `json:"id"`
Api string `json:"api_key"`
}
One nice thing is the tag
syntax behind field declaration. Those string tags control how fields are interpreted during json encoding, it equals to the use of Grape Entity’s name alias.
Now we don’t have ActiveRecord, we can simply use go’s sql
package to do query. sql
package provides a generic interface around SQL databases, similar like Java’s JDBC
. For different database, you can add associated database driver without changing your code. There is a great tutorial on how to use sql package. Since we use MySQL, we can import related package as:
import (
"database/sql"
_ "github.com/go-sql-driver/mysql"
)
We need these following methods:
- get company by api key
- get list of projects under certain company
- get project base on project id:
func GetCompany(db *sql.DB, key string) (Company, int) {
var company_id int
var api_key string
err := db.QueryRow("select id, api from companies where api = ? limit 1", key).Scan(&company_id, &api_key)
switch {
case err == sql.ErrNoRows:
return Company{}, 0
case err != nil:
fmt.Println(err)
return Company{}, -1
default:
return Company{company_id, api_key}, company_id
}
}
func GetProject(db *sql.DB, company_id int, project_id int) (Project, int) {
var (
id int
name string
)
err := db.QueryRow("select id, name from projects where id = ? and company_id = ? limit 1", project_id, company_id).Scan(&id, &name)
switch {
case err == sql.ErrNoRows:
return Project{}, 0
case err != nil:
fmt.Println(err)
return Project{}, -1
default:
return Project{id, name}, id
}
}
func GetProjects(db *sql.DB, companyId int) []Project {
projects, err := db.Query("select id, name from projects where company_id = ?", companyId)
if err != nil {
fmt.Println(err)
}
var (
id int
name string
)
p := make([]Project, 0)
defer projects.Close()
for projects.Next() {
err := projects.Scan(&id, &name)
if err != nil {
fmt.Println(err)
} else {
p = append(p, Project{id, name})
}
}
return p
}
Authentication
The server code sits inside main
function. We first create a martini instance with default settings:
func main() {
... ...
m := martini.Classic()
... ...
In order to do our api key based authentication, we can use Martini’s middleware handler. The middleware handler sit between the incoming http request and the router. It can be used to do the same job as the before
method in our Grape code. The code looks like this:
m.Use(render.Renderer())
m.Use(func(res http.ResponseWriter, req *http.Request, r render.Render) {
api_key := ""
api_key = req.URL.Query().Get("key")
if api_key == "" {
r.JSON(404, map[string]interface{}{"status": "Fail", "error_message": "Need api key"})
} else {
current_company, company_id := GetCompany(db, api_key)
if company_id < 0 {
r.JSON(404, map[string]interface{}{"status": "Fail", "error_message": "Bad api key"})
} else {
m.Map(current_company)
}
}
})
In the above code, we use golang’s URL.Query().Get()
function for http.Request
to retrieve the api key from query string. This function is very useful. There is also a ParseQuery()
function, which returns a map listing the values specified for each key. Those functions can be very helpful when you want to work with POST
request. For more information, please refer net/url package.
Also in the code above, we include Martini’s render middleware, which helps us rendering serialized JSON responses:
m.Use(render.Renderer())
By using Martini’s inject feature, you can pass render.Render
object anywhere you want. And can simply use code like below to generate json response:
r.JSON(404, map[string]interface{}{"status": "Fail", "error_message": "Bad api key"})
One more thing to know is we also used Martini’s so-called Global Mapping:
m.Map(current_company)
In this way, current_company object can be seen globally, and are available to be injected into any Handler’s argument list, you will see the use shortly.
Routes
Routing is not hard. The code is shown as below, very intuitive:
m.Get("/projects", func(current_company Company, r render.Render) {
projects := GetProjects(db, current_company.Id)
r.JSON(200, map[string]interface{}{"status": "Success", "data": projects})
})
m.Get("/projects/:id", func(current_company Company, params martini.Params, r render.Render) {
paramId, err := strconv.Atoi(params["id"])
if err != nil {
r.JSON(404, map[string]interface{}{"status": "Fail", "error_message": err.Error()})
return
}
project, id := GetProject(db, current_company.Id, paramId)
if id > 0 {
r.JSON(200, map[string]interface{}{"status": "Success", "data": project})
} else {
r.JSON(404, map[string]interface{}{"status": "Fail", "error_message": "Project not found"})
}
})
Note that current_company object is injected into argument list of handler functions. Besides, we have martini.Params
that can be used to get params found in route. Here we use it to find project id, and use strconv
package to convert it to integer.
To run your server, just use:
m.Run()
You can always use go’s original http.ListenAndServe
from net/http
package:
http.ListenAndServe(":8080", m)
Conclusion and Benchmark
In this post, we focus on three major parts of writing an api server: Auth, Model and Route, and then compare implementations between Ruby’s Grape and Go’s Martini. I’m still a beginner of Go, and I just learned it in less than a week, but really feel it’s fun and ease to use. As it is shown, Martini provides most of what we need to write a simple api server.
Benchmark is not the important part of this post, but I still did ab
test with -c 10 -n 1000
on local machine. MySQL is used, sample data contains 10 companies, each with 50 projects, so there are 500 projects in total.
Test environment: Macbook Air CPU 1.7GHz Core i5, 8GB DDR3, OSX 10.9.1
ruby -v 2.0.0p247, go version go1.2 darwin/amd64, rails 3.2.16
Grape On Rack
Concurrency Level: 10
Time taken for tests: 16.277 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 2303000 bytes
HTML transferred: 2211000 bytes
Requests per second: 61.44 [#/sec] (mean)
Time per request: 162.769 [ms] (mean)
Time per request: 16.277 [ms] (mean, across all concurrent requests)
Transfer rate: 138.17 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 23 162 33.9 172 289
Waiting: 17 156 33.2 169 278
Total: 23 162 33.9 173 290
Grape On Rails
Concurrency Level: 10
Time taken for tests: 15.902 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 2492000 bytes
HTML transferred: 2211000 bytes
Requests per second: 62.88 [#/sec] (mean)
Time per request: 159.024 [ms] (mean)
Time per request: 15.902 [ms] (mean, across all concurrent requests)
Transfer rate: 153.03 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 0.0 0 0
Processing: 15 158 23.5 170 186
Waiting: 15 158 23.5 170 185
Total: 16 158 23.5 171 186
Go Martini
Concurrency Level: 10
Time taken for tests: 0.900 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 2314000 bytes
HTML transferred: 2211000 bytes
Requests per second: 1110.80 [#/sec] (mean)
Time per request: 9.003 [ms] (mean)
Time per request: 0.900 [ms] (mean, across all concurrent requests)
Transfer rate: 2510.14 [Kbytes/sec] received
Grape on rails uses Puma server, it can get around 63 request/s, where Go server can get 1110 request/s. Go server is faster with no doubt.
Full source code with sample data can be found at github: https://github.com/steventen/grape-vs-martini