Snap: A Haskell Web Framework: Snaplets Tutorial

What Are Snaplets?

A snaplet is a composable web application. Snaplets allow you to build self-contained pieces of functionality and glue them together to make larger applications. Here are some of the things provided by the snaplet API:

  • Infrastructure for application state/environment

  • Snaplet initialization, reload, and cleanup

  • Management of filesystem data and automatic snaplet installation

  • Unified config file infrastructure

One example might be a wiki snaplet. It would be distributed as a haskell package that would be installed with cabal and would probably include code, config files, HTML templates, stylesheets, JavaScript, images, etc. The snaplet’s code would provide the necessary API to let your application interact seamlessly with the wiki functionality. When you run your application for the first time, all of the wiki snaplet’s filesystem resources will automatically be copied into the appropriate places. Then you will immediately be able to customize the wiki to fit your needs by editing config files, providing your own stylesheets, etc. We will discuss this in more detail later.

A snaplet can represent anything from backend Haskell infrastructure with no user facing functionality to a small widget like a chat box that goes in the corner of a web page to an entire standalone website like a blog or forum. The possibilities are endless. A snaplet is a web application, and web applications are snaplets. This means that using snaplets and writing snaplets are almost the same thing, and it’s trivial to drop a whole website into another one.

We’re really excited about the possibilities available with snaplets. In fact, Snap already ships with snaplets for sessions, authentication, and templating (with Heist), This gives you useful functionality out of the box, and jump starts your own snaplet development by demonstrating some useful design patterns. So without further ado, let’s get started.

Snaplet Overview

The heart of the snaplets infrastructure is state management. Most nontrivial pieces of a web app need some kind of state or environment data. Components that do not need any kind of state or environment are probably more appropriate as a standalone library than as a snaplet.

Before we continue, we must clarify an important point. The Snap web server processes each request in its own green thread. This means that each request will receive a separate copy of the state defined by your application and snaplets, and modifications to that state only affect the local thread that generates a single response. From now on, when we talk about state this is what we are talking about. If you need global application state, you have to use a thread-safe construct such as an MVar or IORef.

This post is written in literate Haskell. It uses a small external module called Part2 that is available here. You can also install the full code in the current directory with the command snap init tutorial. First we need to get imports out of the way.

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import           Control.Lens.TH
import           Data.IORef
import qualified Data.ByteString.Char8 as B
import           Data.Maybe
import           Snap
import           Snap.Snaplet.Heist
import           Part2

We start our application by defining a data structure to hold the state. This data structure includes the state of all snaplets (wrapped in a Snaplet) used by our application as well as any other state we might want.

data App = App
    { _heist       :: Snaplet (Heist App)
    , _foo         :: Snaplet Foo
    , _bar         :: Snaplet Bar
    , _companyName :: IORef B.ByteString
    }

makeLenses ''App

The field names begin with an underscore because of some more complicated things going on under the hood. However, all you need to know right now is that you should prefix things with an underscore and then call makeLenses. This lets you use the names without an underscore in the rest of your application.

The next thing we need to do is define an initializer.

appInit :: SnapletInit App App
appInit = makeSnaplet "myapp" "My example application" Nothing $ do
    hs <- nestSnaplet "heist" heist $ heistInit "templates"
    fs <- nestSnaplet "foo" foo $ fooInit
    bs <- nestSnaplet "" bar $ nameSnaplet "newname" $ barInit foo
    addRoutes [ ("/hello", writeText "hello world")
              , ("/fooname", with foo namePage)
              , ("/barname", with bar namePage)
              , ("/company", companyHandler)
              ]
    wrapSite (<|> heistServe)
    ref <- liftIO $ newIORef "fooCorp"
    return $ App hs fs bs ref

For now don’t worry about all the details of this code. We’ll work through the individual pieces one at a time. The basic idea here is that to initialize an application, we first initialize each of the snaplets, add some routes, run a function wrapping all the routes, and return the resulting state data structure. This example demonstrates the use of a few of the most common snaplet functions.

nestSnaplet

All calls to child snaplet initializer functions must be wrapped in a call to nestSnaplet. The first parameter is a URL path segment that is used to prefix all routes defined by the snaplet. This lets you ensure that there will be no problems with duplicate routes defined in different snaplets. If the foo snaplet defines a route /foopage, then in the above example, that page will be available at /foo/foopage. Sometimes though, you might want a snaplet’s routes to be available at the top level. To do that, just pass an empty string to nestSnaplet as shown above with the bar snaplet.

In our example above, the bar snaplet does something that needs to know about the foo snaplet. Maybe foo is a database snaplet and bar wants to store or read something. In order to make that happen, it needs to have a “handle” to the snaplet. Our handles are whatever field names we used in the App data structure minus the initial underscore character. They are automatically generated by the makeLenses function. For now it’s sufficient to think of them as a getter and a setter combined (to use an OO metaphor).

The second parameter to nestSnaplet is the lens to the snaplet you’re nesting. In order to place a piece into the puzzle, you need to know where it goes.

nameSnaplet

The author of a snaplet defines a default name for the snaplet in the first argument to the makeSnaplet function. This name is used for the snaplet’s directory in the filesystem. If you don’t want to use the default name, you can override it with the nameSnaplet function. Also, if you want to have two instances of the same snaplet, then you will need to use nameSnaplet to give at least one of them a unique name.

addRoutes

The addRoutes function is how an application (or snaplet) defines its routes. Under the hood the snaplet infrastructure merges all the routes from all snaplets, prepends prefixes from nestSnaplet calls, and passes the list to Snap’s route function.

A route is a tuple of a URL and a handler function that will be called when the URL is requested. Handler is a wrapper around the Snap monad that handles the snaplet’s infrastructure. During initialization, snaplets use the Initializer monad. During runtime, they use the Handler monad. We’ll discuss Handler in more detail later. If you’re familiar with Snap’s old extension system, you can think of it as roughly equivalent to the Application monad. It has a MonadState instance that lets you access and modify the current snaplet’s state, and a MonadSnap instance providing the request-processing functions defined in Snap.Types.

wrapSite

wrapSite allows you to apply an arbitrary Handler transformation to the top-level handler. This is useful if you want to do some generic processing at the beginning or end of every request. For instance, a session snaplet might use it to touch a session activity token before routing happens. It could also be used to implement custom logging. The example above uses it to define heistServe (provided by the Heist snaplet) as the default handler to be tried if no other handler matched. This may seem like an easy way to define routes, but if you string them all together in this way each handler will be evaluated sequentially and you’ll get O(n) time complexity, whereas routes defined with addRoutes have O(log n) time complexity. Therefore, in a real-world application you would probably want to have ("", heistServe) in the list passed to addRoutes.

with

The last unfamiliar function in the example is with. Here it accompanies a call to the function namePage. namePage is a simple example handler and looks like this.

namePage :: Handler b v ()
namePage = do
    mname <- getSnapletName
    writeText $ fromMaybe "This shouldn't happen" mname

This function is a generic handler that gets the name of the current snaplet and writes it into the response with the writeText function defined by the snap-core project. The type variables ‘b’ and ‘v’ indicate that this function will work in any snaplet with any base application. The ‘with’ function is used to run namePage in the context of the snaplets foo and bar for the corresponding routes.

Site Reloading

Snaplet Initializers serve dual purpose as both initializers and reloaders. Reloads are triggered by a special handler that is bound to the /admin/reload route. This handler re-runs the site initializer and if it is successful, loads the newly generated in-memory state. To prevent denial of service attacks, the reload route is only accessible from localhost.

If there are any errors during reload, you would naturally want to see them in the HTTP response returned by the server. However, when these same initializers are run when you first start your app, you will want to see status messages printed to the console. To make this possible we provide the printInfo function. You should use it to output any informational messages generated by your initializers. If you print directly to standard output or standard error, then those messages will not be available in your browser when you reload the site.

Working with state

Handler b v has a MonadState v instance. This means that you can access all your snaplet state through the get, put, gets, and modify functions that are probably familiar from the state monad. In our example application we demonstrate this with companyHandler.

companyHandler :: Handler App App ()
companyHandler = method GET getter <|> method POST setter
  where
    getter = do
        nameRef <- gets _companyName
        name <- liftIO $ readIORef nameRef
        writeBS name
    setter = do
        mname <- getParam "name"
        nameRef <- gets _companyName
        liftIO $ maybe (return ()) (writeIORef nameRef) mname
        getter

If you set a GET request to /company, you’ll get the string “fooCorp” back. If you send a POST request, it will set the IORef held in the _companyName field in the App data structure to the value of the name field. Then it calls the getter to return that value back to you so you can see it was actually changed. Again, remember that this change only persists across requests because we used an IORef. If _companyName was just a plain string and we had used modify, the changed result would only be visible in the rest of the processing for that request.

The Heist Snaplet

The astute reader might ask why there is no with heist in front of the call to heistServe. And indeed, that would normally be the case. But we decided that an application will never need more than one instance of a Heist snaplet. So we provided a type class called HasHeist that allows an application to define the global reference to its Heist snaplet by writing a HasHeist instance. In this example we define the instance as follows:

instance HasHeist App where heistLens = subSnaplet heist

Now all we need is a simple main function to serve our application.

main :: IO ()
main = serveSnaplet defaultConfig appInit

This completes a full working application. We did leave out a little dummy code for the Foo and Bar snaplets. This code is included in Part2.hs. For more information look in our API documentation, specifically the Snap.Snaplet module. No really, that wasn’t a joke. The API docs are written as prose. They should be very easy to read, while having the benefit of including all the actual type signatures.

Filesystem Data and Automatic Installation

Some snaplets will have data stored in the filesystem that should be installed into the directory of any project that uses it. Here’s an example of what a snaplet filesystem layout might look like:

foosnaplet/
  |-- *devel.cfg*
  |-- db.cfg
  |-- public/
      |-- stylesheets/
      |-- images/
      |-- js/
  |-- *snaplets/*
      |-- *heist/*
          |-- templates/
      |-- subsnaplet1/
      |-- subsnaplet2/

Only the starred items are actually enforced by current code, but we want to establish the others as a convention. The file devel.cfg is automatically read by the snaplet infrastructure. It is available to you via the getSnapletUserConfig function. Config files use the format defined by Bryan O’Sullivan’s excellent configurator package. In this example, the user has chosen to put db config items in a separate file and use configurator’s import functionality to include it in devel.cfg. If foosnaplet uses nestSnaplet or embedSnaplet to include any other snaplets, then filesystem data defined by those snaplets will be included in subdirectories under the snaplets/ directory.

So how do you tell the snaplet infrastructure that your snaplet has filesystem data that should be installed? Look at the definition of appInit above. The third argument to the makeSnaplet function is where we specify the filesystem directory that should be installed. That argument has the type Maybe (IO FilePath). In this case we used Nothing because our simple example doesn’t have any filesystem data. As an example, let’s say you are creating a snaplet called killerapp that will be distributed as a hackage project called snaplet-killerapp. Your project directory structure will look something like this:

snaplet-killerapp/
  |-- resources/
  |-- snaplet-killerapp.cabal
  |-- src/

All of the files and directories listed above under foosnaplet/ will be in resources/. Somewhere in the code you will define an initializer for the snaplet that will look like this:

killerInit = makeSnaplet "killerapp" "42" (Just dataDir) $ do

The primary function of Cabal is to install code. But it has the ability to install data files and provides a function called getDataDir for retrieving the location of these files. Since it returns a different result depending on what machine you’re using, the third argument to makeSnaplet has to be Maybe (IO FilePath) instead of the more natural pure version. To make things more organized, we use the convention of putting all your snaplet’s data files in a subdirectory called resources. So we need to create a small function that appends /resources to the result of getDataDir.

import Paths_snaplet_killerapp
dataDir = liftM (++"/resources") getDataDir

If our project is named snaplet-killerapp, the getDataDir function is defined in the module Paths_snaplet_killerapp, which we have to import. To make everything work, you have to tell Cabal about your data files by including a section like the following in snaplet-killerapp.cabal:

data-files:
  resources/devel.cfg,
  resources/public/stylesheets/style.css,
  resources/snaplets/heist/templates/page.tpl

Now whenever your snaplet is used, its filesystem data will be automagically copied into the local project that is using it, whenever the application is run and it sees that the snaplet’s directory does not already exist. If the user upgrades to a new version of the snaplet and the new version made changes to the filesystem resources, those resources will NOT be automatically copied in by default. Resource installation only happens when the snaplets/foo directory does not exist. If you want to get the latest version of the filesystem resources, remove the snaplets/foo directory, and restart your app.