24 Days of Hackage: snap

The Snap framework is one of a handful of web frameworks available for Haskell, and comes complete with a high performance HTTP server, a rich monad for web programming, composable snaplets, and strong integration with the Heist templating language. In today’s post, we’ll have a look at how simple it is to build applications using Snap.

snap-core

The first library we’ll look at is snap-core, which provides some low level tools for building dynamic websites. Let’s jump straight in to getting some results. A simple web handler is simply a function in anything that is an instance of MonadSnap:

hello :: Snap ()
hello = writeText "Hello, world!"

Hello world doesn’t get any simpler than that! Of course, this is only our handler, the next step is to actually add a route to this handler:

app :: Snap ()
app = route [("/hello", hello)]

This adds a single route to our application, if the user browses /hello, then Snap will use our hello handler, and simply write “Hello, world” back to the browser. Finally, we need to run our application - we’ll use Snap’s server for this:

main :: IO ()
main = quickHttpServe app

That’s it - in 6 lines of code, all of which are straightforward Haskell 98, we have a web app ready to serve requests! snap-core provides a little more, including the ability to parse query/body paramaters, get and set cookies, perform error handling, and manipulating the request and response for the current request.

Snaplets

We haven’t seen anything exactly ground breaking so far, and that’s because snap-core is designed to be simplistic and unassuming. While it’s certainly possible to build complex applications with Snap, the normal process of refactoring and abstracting common patterns is certainly workable, the snap library contains the snaplets abstraction, which is a very powerful way of building up applications.

Snaplets are small little self-contained pieces of functionality that can be combined together to build larger applications. Snaplets were introduced in Snap 0.6, and already there are many on Hackage - Snaplets for Riak, postgresql-simple, Recaptcha, and more. Let’s see how snaplets work:

data App = App { _db :: Snaplet Postgres }

makeLenses ''App

initApp :: SnapletInit App App
initApp = makeSnaplet "myapp" "My application" Nothing $
  App <$> nestSnaplet "db" db pgsInit
      <* addRoutes [("/hello/:id", helloDb)]

helloDb :: Handler App App ()
helloDb = do
  Just mUId <- getParam "id"
  userName <- with db $
    listToMaybe <$>
      query "SELECT name FROM users WHERE id = ?" (Only mUId)
  writeText $ maybe "User not found" ("Hello, " ++ ) userName

main :: IO ()
main = serveSnaplet defaultConfig initApp

Here I’ve created my App data type, which is passed around through all requests. We initialize the application using initApp, which creates a Snaplet itself, with a Snaplet Postgres nested inside it. This will let us perform database queries. Finally, I’ve added a single route to the application - this route is a bit different from the one we saw before, as it also binds a variable - that’s what :id is for. This means that we can go to pages like /hello/10 and our helloDb handler will be fired, with id set to 10.

Our helloDb handler is also a bit more complex now. I first retrieve the id parameter, here somewhat circumventing the safety of Maybe. We then use with to “move into” our database Snaplet, which now lets us use things in the Handler b Postgres monad - in this case, query. Finally, we return this back up to our helloDb application, and then write out a greeting.

Snap is my favourite choice for building web applications - I find the beautiful combination of simplicity and elegance in snap-core is extremely powerful when combined with the snaplets abstraction. If you’re looking to do some web programming with Haskell over the weekend, and want a framework with a low barrier to entry, you could do far worse than give Snap a try.


You can contact me via email at ollie@ocharles.org.uk or tweet to me @acid2. I share almost all of my work at GitHub. This post is licensed under a Creative Commons Attribution-NonCommercial-NoDerivs 3.0 Unported License.