Introducing Growler, a simple HTTP server toolkit
For the past few years, I've always used scotty
as my tool of choice when building
simple servers for Haskell. However, I've come to take issue with some of the design
choices that scotty
takes, a few of which include:
- rolling its own error mechanism
- The
next
function, which allows you to switch execution to the next handler that might handle a request. - odd choices for data types that involved unnecessary amounts of coersion
- always loading request bodies into memory
- perhaps a wee bit too minimalistic
In light of these issues, I set out to build a toolkit that keeps the best parts of
scotty
while ameliorating some of the drawbacks that frequently caused me grief and
while adding a few features of my own.
What's gone
Scotty exports a duplicate API for using it with monad transformers (ScottyT
) and without (ScottyM
).
I realize that it's largely for historical reasons that both versions exist, but it always suffices for me
to use the transformer version. If I don't have a special base monad, I just use IO, which is what ScottyT
really
does anyways.
The next
function is gone, as I mentioned above. It's a really bizarre concept to me that you'd have control flow
leave off in one request-handling function and start off in a different one. I want all of my logic in one place and
to be responsible for crafting my routes in such a way that they don't overlap. Additionally, the next
function implements
this control flow transfer by throwing an exception, which is not particularly idiomatic Haskell to me.
What's changed
The ActionT
monad transformer is the HandlerT
monad transformer in Growler terminology. I just think the name is more
sensible.
param
& params
don't read form parameters, just pattern matches & query param matches in the URL. The short explanation
is that I prefer leaving it up to the library user to determine whether they want to actually consume the request body or not.
Headers aren't coerced into lazy Text values. They are strict ByteStrings in WAI, so using lazy Texts necessitates conversion on the way in and on the way out, which turned out to be relatively expensive for my workloads.
capture
, regex
, and literal
for constructing routes are all implemented in terms of function
instead of as intermediate data
structures. This provided some modest performance improvements.
What's new
Restructuring the routing gave some leeeway for some new tricks: mount
& handlerHook
.
mount
lets you avoid redeclaring the same base sections of routes in a nicely composable way. As an example, where you would have needed to write:
get "/users/:username/settings" userSettingsPage
get "/users/:username/comments" userCommentsPage
get "/users/:username" userInfoPage
...
You can now write:
mount "/users/:username" $ do
get "/settings" userSettingsPage
get "/comments" userCommentsPage
get "/" userInfoPage
There! That's a little DRYer.
The other handy addition is handlerHook
, which lets you declare middleware for handlers that's
aware of all of the context provided by your monad transformer stack.
One way that I currently use this is with mount
to enforce authentication for subsections of the site:
mount "/admin" $ do
... -- admin-specific routes
handlerHook enforceUserIsAdmin
The nice thing here is that the middleware is only run on requests in the mount block, instead of on every request across the entire site.
Example (Untested)
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Web.Growler
main = growl id notFound $ do
get "/" $ text "Hello, world!"
mount "/:user" $ do
get "/" (param "user" >>= text)
get "/settings" $ text "This would be a settings page"
"I want to use it!"
Great! It's still a little rough around the edges, but it's on Hackage. In particular, the documentation could use some work, but if you're familiar with Scotty's idioms, they should more or less translate directly.
"I want to contribute!"
Please do! You can find the source code on GitHub. Feel free to file any issues that you encounter or to submit pull requests.