The Martini Web Framework

A gentle introduction

27 February 2014

Eric Gravert

Lead Software Engineer, ModCloth, Inc.

But first, a bit of background...

Originally, we had net/http, and life was good

package main

import (
	"io"
	"net/http"
	"time"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "Welcome to the Go Steel Programmers Meetup!")
    })
    http.HandleFunc("/show-time", func(w http.ResponseWriter, req *http.Request) {
        io.WriteString(w, "<h1>"+time.Now().String()+"</h1>")
    })
    http.ListenAndServe(":8090", nil)
}

net/http continued...

package main

import (
	"io"
	"net/http"
)

func main() {
    s := &Server{}
    http.ListenAndServe(":8090", s)
}

type Server struct{}

func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "(╯°□°)╯︵ ┻━┻")
}

net/http conclusion

net/http is an elegantly written, highly customizable, http server.
It is a great starting point for building your services and web applications.

Introducing....

Martini!!

What is Martini?

Our first example

package main

import (
    "net/http"
    "time"

    "github.com/codegangsta/martini"
)

func main() {
    m := martini.Classic()
    m.Any("/time", func() string {
        return "The time is: " + time.Now().Format(time.Kitchen)
    })

    m.Post("/create", func() (int, string) {
        return http.StatusCreated, "Thanks for your feedback"
    })

    m.Get("/hello/:name", func(p martini.Params) string {
        return "Hello, " + p["name"]
    })
    m.Run()
}

Martini provides this functionality by adding the following features

Routing

Associates an HTTP Method and URL pattern to a series of Handlers

package main

import (
	"net/http"
	"time"

	"github.com/codegangsta/martini"
)

func main() {
	m := martini.Classic() // HLCLASSIC
    m.Any("/time", func() string {
        return "The time is: " + time.Now().Format(time.Kitchen)
    })

    m.Post("/create", func() (int, string) {
        return http.StatusCreated, "Thanks for your feedback"
    })

    m.Get("/hello/:name", func(p martini.Params) string {
        return "Hello, " + p["name"]
    })
	m.Run()
}

Handlers

Are callable functions that are mapped to a given request by the router

Stacking handlers

// router configuration
m.Post("/book/new", Authorize, AddBook)

// leverage sessions package to see if user
// has access to the given resource
func Authorize(session sessions.Session) {
  // validate session cookie
}

func AddBook(req *http.Request, db sql.DB) {
  // parse the request and create a new book
}

Services

Services are the objects that can be injected into the handler argument list
they can be provided at two levels, globally, and per-request

st := &Storer{}
m.Map(st) // this service will be available to all handlers as *Storer
st := &MysqlStorer{}
m.MapTo(st, (*Storer)(nil)) // Maps type MysqlStorer to the Storer interface
// within a handler (global or request lvl)
func MyHandler(c martini.Context, s sessions.Session, ul *UserLookup) {
  user := ul.LookupUser(s.Get("user-token"))
  c.Map(user)
}

Middleware

store := sessions.NewCookieStore([]byte("secret123"))
m.Use(sessions.Sessions("my_session", store)) // Sessions returns a function.
                                              // The function is a closure
                                              // that captures the session name and store

Introducing Martini-contrib

Render Middleware

Simplifies rendering HTML and JSON templates

// add a middlware handler to enable rendering
m.Use(render.Renderer(render.Options{Layout: "layout"}))
func ShowNotes(r render.Render, ns NoteStorer) {
    notes := ns.All()
    r.HTML(200, "notes", notes)
}

Binding Middleware

Binding middleware maps the values from an incoming request to a struct and passes it to the handler

// Tell the binder to populate the Note struct with form data
m.Post("/create", binding.Bind(Note{}), NewNote)

// The Note is now available as a handler argument
func NewNote(note Note, r render.Render, ns NoteStorer) {
    ns.Add(note)
    notes := ns.All()
    r.HTML(http.StatusCreated, "notes", notes)
}

Static

Part of ClassicMartini, the static middleware serves up static assets that reside in a specified directory.

m.Use(martini.Static("assets")
m.Use(martini.Static("public")

And many more

See the full list at github.com/martini-contrib

Testing

func TestShowingNotes(t *testing.T) {

    m := Shaken() // a method on main that configures, but doesnt `Run` the martini instance
    response := httptest.NewRecorder()
    request := NewRequest("GET", "/", "", "")

    m.ServeHTTP(response, request)

    expect(t, http.StatusOK, response.Code)
}

Testing a POST

func TestCreatingNotes(t *testing.T) {
    var startRows, endRows int

    db := SetupDb()

    defer db.QueryRow("DELETE FROM notes")

    db.QueryRow("SELECT COUNT(*) FROM notes").Scan(&startRows)
    m := Shaken() // a method on main that configures, but doesnt `Run` the martini instance

    response := httptest.NewRecorder()
    request := NewRequest("POST", "/create", "application/x-www-form-urlencoded; param=value", "note=always%20test%20your%20app&tags=")
    m.ServeHTTP(response, request)

    // validate http status
    expect(t, http.StatusCreated, response.Code)

    // validate row counts in db
    db.QueryRow("SELECT COUNT(*) FROM notes").Scan(&endRows)
    expect(t, startRows+1, endRows)
}

Putting it all together

Demo time!
Second Brain

Final Thoughts

Further Reading

Thank you

Eric Gravert

Lead Software Engineer, ModCloth, Inc.

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)