Your First Web Application with Spock

Show Notes

The Spock web framework for Haskell gives you a light but complete foundation to build web servers on, be it for traditional server-side rendered applications, or APIs for single-page applications. Compared to the Scotty framework, Spock is slightly richer in its feature set, and is more opinionated regarding the application architecture. In this episode we explore the basics of Spock and in-memory server state, as we build a note keeper application.

We are starting with an empty project called notekeeper, and a Cabal file specifying an executable. To get started, we add dependencies for Spock, http-types, text, and mtl.

name: notekeeper version: 0.1.0.0 category: Web build-type: Simple cabal-version: >=1.10 executable notekeeper main-is: Main.hs build-depends: base >=4.10 && <4.11 , Spock , http-types , text , mtl hs-source-dirs: src default-language: Haskell2010

In Main.hs , we start out by importing the Web.Spock and Web.Spock.Config modules. Those are all we need to get a basic server running.

module Main where import Web.Spock import Web.Spock.Config

The function we need in our definition of main is runSpock , which has the type:

runSpock :: Port -> IO Middleware -> IO ()

We run our server on port 8080, with the middleware returned by a function called spock . It takes a configuration, and our application. We’ll leave those as typed holes for now, and see what we need.

main :: IO () main = do runSpock 8080 (spock _cfg _app)

The _app hole tells us we need some value of type SpockM conn0 sess0 st0 () , which is the Spock monad for defining an application. It also tells us that the type variables used as parameters are ambiguous.

Found hole: _app :: SpockM conn0 sess0 st0 () Where: ‘conn0’ is an ambiguous type variable ‘sess0’ is an ambiguous type variable ‘st0’ is an ambiguous type variable

The _cfg hole tells us that we need a value of type SpockCfg conn0 sess0 st0 , and that the same type variables as we saw before are ambiguous. There is a connection between the type parameters of SpockM and SpockCfg .

Found hole: _cfg :: SpockCfg conn0 sess0 st0 Where: ‘conn0’ is an ambiguous type variable ‘sess0’ is an ambiguous type variable ‘st0’ is an ambiguous type variable

We begin by defining app locally, not doing any routing, only returning () .

main :: IO () main = do let app = return () runSpock 8080 (spock _cfg app )

This would work, but the type variables are still ambiguous for the configuration. If we move the definition of app to the top-level, and give it an explicit type signature, we can pin down those type arguments.

The type SpockM is parameterized by four types:

the database connection type ( conn )

) the session type ( sess )

) the state type ( st )

) the monadic return value’s type

Initially, we won’t have any database, sessions, or state, so we use the () type for all of them.

app :: SpockM () () () () app = return ()

Now that we’ve specified which types we are using in our application, the typed hole _cfg has no ambiguous variables. To construct a configuration value, we use the defaultSpockCfg , taking a session configuration, a pool or connection for a database, and an initial state. The database parameter cannot be a () value, and we’ll leave it as a typed hole to illustrate why.

Found hole: _db :: PoolOrConn ()

PoolOrConn is parameterized by () , and there is only one value with that type: the PCNoDatabase constructor defined by Spock. Using that, we obtain a configuration for our Spock application, and we have a rather useless, but working web server.

main :: IO () main = do cfg <- defaultSpockCfg () PCNoDatabase () runSpock 8080 (spock cfg app)

To run the web server, and have it reload on source code changes, we can use the excellent ghcid tool. By default, it only reloads your code and prints any type errors, but if we override the test command using -T , we can have it run the main function when the code successfully compiles.

$ ghcid -T :main Running test... Spock is running on port 8080

In our web browser, we can see that we get a “404 Not Found” when requesting http://localhost:8080. We’ll keep ghcid running in the background.

Defining Routes

Before we begin defining routes, we’ll create a type alias for our application type to factor out the SpockM details. Server takes a single type argument a , the monadic return value type. We use it as the type of our app definition.

type Server a = SpockM () () () a app :: Server () app = return ()

We define a route for GET requests to the root path, responding with a text greeting.

app :: Server () app = get root (text "Hello!")

Now we get a type error, explaining that we can’t pass a regular String to the text function. We enable OverloadedStrings to construct a Text value instead.

{-# LANGUAGE OverloadedStrings #-}

Reloading the web browser, we see our greeting “Hello!” displayed.

A plain text response.

Rendering HTML

Responding with plain text in a web application is a little dull. Instead, we’ll respond with HTML using the html function, and some lovingly hand-crafted markup.

app :: Server () app = get root ( html "<h1>Hello!</h1>" )

Now get a proper heading, at least.

An HTML response.

Constructing HTML markup in strings is not great, though. Instead, we’ll use Lucid, an embedded DSL for HTML markup. In our Cabal file, we add the dependency lucid , along with Spock-lucid , a small library integrating Spock and Lucid.

executable notekeeper main-is: Main.hs build-depends: base >=4.10 && <4.11 , Spock , http-types , text , mtl , lucid , Spock-lucid hs-source-dirs: src default-language: Haskell2010

Back in Main.hs , we import the lucid function from Web.Spock.Lucid , along with the entire Lucid module.

import Web.Spock.Lucid (lucid) import Lucid

Now we can use lucid instead of html , and the Lucid DSL to construct our markup. Using do notation, we can define sibling elements, which in our case will be a heading and a paragraph.

app :: Server () app = get root $ lucid $ do h1_ "Hello!" p_ "How are you today?"

Reloading the web browser, we see the two elements rendered.

HTML constructed using Lucid.

Keeping Notes with Server State

We are ready to implement the note keeping functionality of our application. Instead of using a server state of type () , we’ll define and use the ServerState and Note data types.

The server state consists of an IORef holding a list of Note values. An IORef is a variable that can be mutated atomically in the IO monad.

newtype ServerState = ServerState { notes :: IORef [Note] }

A note has an author, and contents, both of type Text .

data Note = Note { author :: Text, contents :: Text }

To use Text and IORef we need to import them.

import Data.Text (Text) import Data.IORef

Finally, we change the Server type alias to use ServerState .

type Server a = SpockM () () ServerState a

Navigating to the next type error we get, we see that we cannot use an application requiring a state of type ServerState with a configuration with state of type () .

Couldn't match type ‘ServerState’ with ‘()’ Expected type: SpockM () () () () Actual type: Server ()

We construct a ServerState by mapping the constructor over a newIORef action, with an empty list of notes, and use it in our configuration.

main :: IO () main = do st <- ServerState <$> newIORef [] cfg <- defaultSpockCfg () PCNoDatabase st runSpock 8080 (spock cfg app)

It compiles again! To have something that we can render, we add two notes to the initial state.

main :: IO () main = do st <- ServerState <$> newIORef [ Note "Alice" "Must not forget to walk the dog." , Note "Bob" "Must. Eat. Pizza!" ] cfg <- defaultSpockCfg () PCNoDatabase st runSpock 8080 (spock cfg app)

Now we have something to render, so we change the greeting markup to instead list the notes in our state. We’ll use an unordered list, constructed using ul_ , and for each note render a list item, containing the note author and contents.

app :: Server () app = get root $ lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note)

The forM_ function is not in scope, so we need to import it.

import Control.Monad (forM_)

Also, we don’t have a list of notes available. We need to read it from the IORef in our server state. The getState action in Spock returns our server state, from which we can extract and read the IORef .

app :: Server () app = get root $ do notes' <- getState >>= (liftIO . readIORef . notes) lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note)

The usage of getState might remind you of ask from the Reader monad.

The readIORef action has to be lifted into the ActionCtxt , which is the monadic type for Spock routes. Thus, we import and use liftIO .

import Control.Monad.IO.Class (liftIO)

No more errors, so we refresh the web browser and see our list of notes rendered.

An unordered list of notes.

Posting New Notes with a Form

Cool! But to make the application useful, we want to add a form for creating new notes. We add a new route, accepting POST requests to the root path.

app :: Server () app = do get root $ do notes' <- getState >>= (liftIO . readIORef . notes) lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note) post root $ do ...

We will use the param' function, that given a field name extracts a field from the posted form data, or responds with an error if the field is missing.

param' :: forall p (m :: * -> *) ctx. (FromHttpApiData p, MonadIO m) => Text -> ActionCtxT ctx m p

We extract the author and contents parameters from the form data. We also get the IORef from our state, so that we can modify it.

app :: Server () app = do get root $ do notes' <- getState >>= (liftIO . readIORef . notes) lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note) post root $ do author <- param' "author" contents <- param' "contents" notesRef <- notes <$> getState ...

In IO , we use atomicModifyIORef' , the strict version of atomicModifyIORef , to atomically update the list of notes based on the existing list.

atomicModifyIORef' :: IORef a -> (a -> (a, b)) -> IO b

Our return value is () , and our modified state will be the list of notes with the new note appended.

app :: Server () app = do get root $ do notes' <- getState >>= (liftIO . readIORef . notes) lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note) post root $ do author <- param' "author" contents <- param' "contents" notesRef <- notes <$> getState liftIO $ atomicModifyIORef' notesRef $

otes -> (notes <> [Note author contents], ())

To use (<>) , the associative semigroup operation, we import it from the Semigroup module.

import Data.Semigroup ((<>))

Finally, we redirect the user agent to / , the root of our web application. This is a common practice known as the Post/Redirect/Get pattern.

app :: Server () app = do ... post root $ do author <- param' "author" contents <- param' "contents" notesRef <- notes <$> getState liftIO $ atomicModifyIORef' notesRef $

otes -> (notes <> [Note author contents], ()) redirect "/"

Rendering a Form

Below the list of existing notes, we’ll display a heading, and form with post as its method. The form will contain two labels; one for the author input, and one for the contents textarea. Last, it will have a submit button.

app :: Server () app = do get root $ do notes' <- getState >>= (liftIO . readIORef . notes) lucid $ do h1_ "Notes" ul_ $ forM_ notes' $

ote -> li_ $ do toHtml (author note) ": " toHtml (contents note) h2_ "New Note" form_ [method_ "post"] $ do label_ $ do "Author: " input_ [name_ "author"] label_ $ do "Contents: " textarea_ [name_ "contents"] "" input_ [type_ "submit", value_ "Add Note"]

Refreshing the web browser, we see our form. It’s not pretty, but styling is outside the scope of this video.

The rendered form for adding new notes.

We can post a new note, and see it as the browser gets redirected back to the root path and performs a GET request.

A new note is added and rendered after the redirect.

Nice, we have a note keeper application!

Summary

We have used Spock and it’s built-in support for server state. In a real application you’d probably use a persistent database for the notes, by using a database connection pool instead of PCNoDatabase . In this example application, we used server state to implement an in-memory database of notes. In a future episode we could replace it with a persistent database.

That’s all for today. Thanks for watching!

Credits