All of our services (external REST APIs and internal services) are written in Go. Like others, we wanted to factor out repeated code, but found it hard to do without making things messy or hiding the flexibility of the built-in net/http interfaces. After evaluating many “REST frameworks” for Go, we found some that seemed to have good ideas, but nothing that met our needs well enough. Using these as inspiration and drawing on experience with other languages and their frameworks, we created Siesta. We believe it improves on other projects with similar goals, and is worth trying.
Siesta is our third “REST framework;” the others are ugly internal-only failures we will never open source. Siesta is designed to accomplish the following:
- Handle common tasks such as defining and validating inputs, which can be in the URL or query string. These are strongly typed and flexible.
- More sophisticated routing than provided by net/http.
- Chaining together handlers in a “middleware” fashion to factor out repeated code, compose it easily, and keep the actual handler function for each route clean and clear. This includes things like marshal/unmarshal, setup, and logging.
- Pass variables in a “context” between middleware handlers.
- Make services and each bit of middleware logic easier to test.
- Be idiomatic. For example, parameter definitions draw inspiration from the flag package.
Something that we noticed very early on was that most frameworks try to do too much. Often times frameworks either have features that we don’t need or impose too many limitations on how we build REST services. In particular, frameworks tend to move away from Go’s native net/http usage. We tried to make Siesta as lightweight and minimal as possible while enhancing what is already provided by net/http with idiomatic helpers.
Siesta has helped us in many ways:
- Middleware greatly reduced the amount of duplicated code.
- Overall API design has become more modular, which has greatly improved unit tests.
- Its flexibility has allowed us to do things that weren’t possible with our previous framework.
Siesta has been open-source from the beginning, but we didn’t want to publicize it until we were satisfied that it’s a good enough solution. We’re at that point now and ready to share. We believe it improves on other projects with similar goals, and is worth broad study and perhaps adoption among Gophers writing HTTP services and APIs.
Design
One of the main principles that we had in mind when designing another REST framework was that it should be as simple as possible. A net/http
HandlerFunc
has the following signature:
func (w http.ResponseWriter, r *http.Request)
Often times, just using
HandlerFuncs
leads to a lot of replicated code. In order to deduplicate code, we introduced a context parameter to our handler functions to easily chain them together. Siesta’s use of a context parameter is inspired by a Go concurrency pattern, which is described on the
Go blog. Ours is much simpler; it’s an interface that has a
Set
and
Get
method, which is trivially implemented as a
map[string]interface{}
.
type Context interface { Set(string, interface{}) Get(string) interface{} }
For example, we assign each request an ID so we can easily identify and trace requests in our logs.
func (c siesta.Context, w http.ResponseWriter, r *http.Request) { requestID := c.Get("request-id") // ... if err != nil { log.Println("[%v] %v", requestID, err) return } // ... }
We’ve added a couple of handler function “chains” in Siesta to help with composition. There is a single handler assigned to each route, but we have “pre” and “post” chains surrounding the route handlers that are always executed. The “pre” chain handles things like identifying requests, setting up database handles, and logging requests. The “post” chain has code to actually send data back to the client, for example.
There are times when something goes wrong and you have to quit executing a chain in the middle, perhaps because the request failed some sort of validation. The final piece was to add a “quit” function to signal that the chain should be stopped. The implementation allows us to support termination of certain handlers and not others, allowing us to bypass certain sections of code during errors but still be able to send data back to the client.
Our complete internal signature of a handler became this:
func (c siesta.Context, w http.ResponseWriter, r *http.Request, quit func())
The best thing about Siesta is that it composes handler functions really well. We provide a
siesta.Compose()
helper to compose multiple handler functions together. The
Context
and
quit
parameters are also optional, so you are free to compose different types of handler functions together.
Finally, Siesta makes it really easy to use URL and query string parameters with a flag-like interface.