A dead-simple web stack in Haskell

November 16, 2019
« Previous post   Next post »

Haskell has a proliferation of libraries to choose from for all of your basic backend needs, from logging to database access to routing and web server definition. It's nice to have the freedom of choice, but if you're just starting out, the amount of decisions needed can be overwhelming. Maybe you're not even confident enough yet that you'll be able to discern important differences between all the choices out there. You need to query your database. Do you want the strong column name guarantees and deep SQL embedding that Squeal gives you, or would you prefer the relative simplicity of Opaleye while still getting type safety? Or maybe it would be better to just use postgresql-simple and keep things really easy? Or what about using Selda? Or what about...

As a way to show that you don't need to spend tons of agonizing about how advanced your stack is, as well as a learning opportunity for myself, I've written an example web application using the simplest libraries I could find. If you're not sure how to go about building real applications in Haskell, why not try learning from it? I've deliberately tried to keep the codebase as simple as I could. You can find the source code here.

Let's go over what libraries I chose and what you should expect from them, as well as what the application is.

Okay, so what is this web app anyways?

It's a site where users can create custom timers and notes for themselves.

For instance, one use case might be cooking: Someone might need to set up various different timers to track the progress of different reagents, as well as notes to themselves about things that they have to be careful of, things that can be improved for the next time they make a recipe, etc. Another use case might be someone playing a MOBA, like League of Legends or Dota 2, where they could have a page open in a second monitor to track key cooldowns, as well as notes to themselves about how to macro versus the enemy composition and cooldowns to keep in mind while teamfighting.

This lets me demonstrate:

  • Sessions, since users should be able to refresh the page, or leave and come back, and still see the same elements.
  • Persistence and database access, since we need to save the timers and notes for each user. Another subtlety is that timers should retain their remaining time. (What if it's a 30 minutes timer and the user accidentally closes the page?)
  • Run-time configuration, since we can't hardcode database connection info.
  • Logging. Self-explanatory for a web application.

Here's the application source code.

Okay, so what are the libraries?

Routing and web server: Spock

Spock ends up being the library of choice here because of its ease of use. If you're ever used Ruby's Sinatra, Spock should feel very similar.1 Spock also comes with session handling out of the box, which is very nice.

For instance, defining a server with a few routes to send back HTML and JSON might look like this:

{-# LANGUAGE OverloadedStrings #-}

import Web.Spock as Spock
import Web.Spock.Config as Spock
import Data.Aeson as A

main :: IO ()
main = do
  spockCfg <- defaultSpockCfg () PCNoDatabase ()
  runSpock 3000 $ spock spockCfg $ do
    get root $ do
      Spock.html "<div>Hello world!</div>"
    get "users" $ do
      Spock.json (A.object [ "users" .= users ])
    get ("users" <//> var <//> "friends") $ \userID -> do
      Spock.json (A.object [ "userID" .= (userID :: Int), "friends" .= A.Null ])

  where users :: [String]
        users = ["bob", "alice"]

Database access: postgresql-simple

postgresql-simple basically just lets you run raw SQL queries against your database, with a minimum of extra frills, such as protection against injection attacks. It does what you would expect and nothing more.

{-# LANGUAGE OverloadedStrings #-}

import Database.PostgreSQL.Simple

userLoginsQuery :: Query
userLoginsQuery =
  "SELECT l.user_id, COUNT(1) FROM logins l GROUP BY l.user_id;"

getUserLogins :: Connection -> IO [(Int, Int)]
getUserLogins conn = query_ conn userLoginsQuery

Configuration: configurator

configurator reads configuration files and parses them into Haskell datatypes. It's a bit more featureful than your usual config file reader; if you're used to flat configuration files, configurator has a few more tricks up its sleeves. Config attributes can be nested for grouping, and configurator also provides hot reloading on config file change, if you need that.

# An example config file.

app_name = "The Whispering Fog"

db {
  pool {
    stripes = 4
    resource_ttl = 300
  }

  username = "pallas"
  password = "thefalloflatinium"
  dbname = "italy"
}
{-# LANGUAGE OverloadedStrings #-}

import Data.Configurator as Cfg
import Database.PostgreSQL.Simple

data MyAppConfig = MyAppConfig
  { appName :: String
  , appDBConnection :: Connection
  }

getAppConfig :: IO MyAppConfig
getAppConfig = do
  cfgFile <- Cfg.load ["app-configuration.cfg"]
  name <- Cfg.require cfgFile "app_name"
  conn <- do
    username <- Cfg.require cfgFile "db.username"
    password <- Cfg.require cfgFile "db.password"
    dbname <- Cfg.require cfgFile "db.dbname"
    connect $ defaultConnectInfo
      { connectUser = username
      , connectPassword = password
      , connectDatabase = dbname
      }
  pure $ MyAppConfig
    { appName = name
    , appDBConnection = conn
    }

Logging: fast-logger

fast-logger provides a reasonably simple-to-use logging solution. In the example web application, I just use it to print to stderr, but it has options to log to files as well. While it has a lot of types, for the most part you'll want to define helper functions that just take in a LoggerSet and the message you want to log.

import System.Log.FastLogger as Log

logMsg :: Log.LoggerSet -> String -> IO ()
logMsg logSet msg =
  Log.pushLogStrLn logSet (Log.toLogStr msg)

doSomething :: IO ()
doSomething = do
  logSet <- Log.newStderrLoggerSet Log.defaultBufSize
  logMsg logSet "message 1"
  logMsg logSet "message 2"

Generating HTML: blaze-html

While there wasn't much HTML that needed to be generated from the backend on this project, it's worth mentioning blaze-html for the parts that I did need.

It's essentially just an shallow embedding of HTML into a Haskell DSL. If you can write HTML, you already know how to use this library.

{-# LANGUAGE OverloadedStrings #-}

import Data.ByteString.Lazy

import Text.Blaze.Html5 as HTML
import Text.Blaze.Html5.Attributes as HTML hiding ( title )
import Text.Blaze.Html.Renderer.Utf8 as HTML

dashboardHTML :: HTML.Html
dashboardHTML = HTML.html $
  HTML.docTypeHtml $ do
    HTML.head $ do
      HTML.title "Timers and Notes"
      HTML.meta ! HTML.charset "utf-8"
      HTML.script ! HTML.src "/js/bundle.js" $ ""
    HTML.body $ do
      HTML.div ! HTML.id "content" $ ""

dashboardBytes :: ByteString
dashboardBytes = HTML.renderHtml dashboardHTML

Building and frontend: make + npm

Yeah, yeah, these aren't libraries. Still, we needed some sort of JavaScript frontend, since the timers have to update in realtime. Webpack produced the JS bundle, while Make assembled the final application output.

I won't talk too much about these. There are plenty of resources about using both of these tools elsewhere.

Do I have to use these?

No, of course not. If you're exploring Haskell in the first place, you're probably naturally curious. Don't let me hold you down or dictate what you should do. While this application works, many parts of it would be considered unidiomatic for production Haskell. For instance, many Haskellers would likely use Servant instead of Spock for defining the API endpoints. If you're interested in other library choices, you should absolutely follow your inclinations.

Think of these libraries and this application as a starting point. I encourage you to use this code as a learning opportunity and figure out how it works, then start tinkering. One of the beautiful things about Haskell is how easy it is to refactor or upgrade without breaking things. Once you've got a handle on this application, why not try replacing parts of it with more advanced libraries that give you more guarantees, as a way of incrementally learning Haskell?

  • Upgrade the DB access to use a type-safe query library instead of postgresql-simple. I recommend Opaleye!
  • Upgrade the API definition to use Servant instead of Spock.
  • Add automated testing using QuickCheck or hedgehog. For instance, you could test the property that every error response from the server also sends back a JSON error message.

And you could even try replacing the frontend and build system.

  • Upgrade the frontend code to use PureScript or Elm instead of vanilla JavaScript.
  • Upgrade the build system to use Shake instead of Make to make things more robust.

Here's the link to the example application source code if you missed it.

Want advice on where to start tinkering? Found this useful? Still have questions? Talk to me!


You might also like

« Previous post   Next post »

Before you close that tab...


Footnotes

↥1 Which makes sense, since Spock’s documentation states that it was inspired by Sinatra!