Resource-ful Handlers

It seems that most routers for web apps provide an API that gives equal importance to the path and the method, which in my opinion is totally wrong. A path names a resource whereas the method is an action you can (possibly) perform on that resource.

Routing should happen in layers:

  1. At the top level you are finding the correct resource for the request.
  2. Then work out the action to perform; GET = retrieve, POST = create, etc.
  3. Finally content negotiation using the Content-Type and Accept-* headers.

The third layer is not always needed, but the first two generally are. But most of the time they are defined as if they are equals.

Previously I was writing my applications with routing like,

booksHandler := handlers.Books(db, es)
r.Path("/books/{id}").Methods("GET").Handler(persona.Protect(booksHandler.Get))
r.Path("/books/{id}").Methods("PATCH").Handler(persona.Protect(booksHandler.Update))
r.Path("/books/{id}").Methods("DELETE").Handler(persona.Protect(booksHandler.Delete))

A struct is created with different fields for the handlers which should be used for the various http methods. But there is no reason for the top-level routing layer to know about whether this supports DELETE, it should simply say /books/{id} is a book.

Contrast with the current version:

route.Handle("/books/:id", persona.Protect(handlers.Books(db, es)))

Now the different actions that can be made on a book are defined in a separate handler:

func Books(db data.Db, es *events.Source) http.Handler {
  h := booksHandler{db, es}
  return mux.Method{
    "GET": h.Get(),
    "PATCH": h.Update(),
    "DELETE": h.Delete(),
  }
}

This uses my mux library which defines some simple map[string]http.Handler types that implement http.Handler. It now acts like a resource, so the router is left the simple job of mapping request paths to resources.