Making A Website With Haskell

updated: July 16, 2014

Warning: this post is old and out of date.

This is a guide to building and deploying a simple website using Haskell.

We will use:

Scotty is Haskell's version of Sinatra. It also uses the same web server as Yesod (Warp) so it's quite fast.

Getting set up

Before we start, here's how I like to set up a Haskell project:

1. Use a Cabal sandbox.

This creates an isolated environment and prevents you from running into dependency hell.

To use a Cabal sandbox, you need Cabal version 1.18+. Then:

cd project_dir
cabal sandbox init

That's it! Now all your packages will be installed in a sandboxed environment, so they won't screw up any packages on your machine.

2. Make a cabal file for your project.

Here's a simple one you can use (save it as todo.cabal):

name:                todo
version:             0.0.1
synopsis:            My awesome todo-list app
license:             MIT
author:              Aditya Bhargava
category:            Web
build-type:          Simple
cabal-version:       >=1.8

executable todo
  main-is:             Main.hs
  -- other-modules:
  build-depends:       base ==4.6.*
                     , wai
                     , warp
                     , http-types
                     , resourcet
                     , scotty
                     , text
                     , bytestring
                     , blaze-html
                     , persistent ==1.3.*
                     , persistent-template ==1.3.*
                     , persistent-sqlite ==1.3.*
                     , persistent-postgresql ==1.3.*
                     , monad-logger ==0.3.0
                     , heroku
                     , transformers
                     , wai-middleware-static
                     , wai-extra
                     , time

Hello World

Save this as Main.hs:

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main = scotty 3000 $ do
  get "/" $ do
    html "Hello World!"

Now make and run it:

cabal install

Go to http://localhost:3000 and you should see your new Haskell site!


You might see a build failure with a cryptic message...something like:

cabal: Error: some packages failed to install:
monad-logger- failed during the building phase. The exception was:
ExitFailure 1

ExitFailure 1? Not a very helpful message. You can get a better error message by trying to install the package yourself using cabal install monad-logger- It's possible that something broke in this version of the package...downgrading to a different version might fix your issue. For example, cabal install monad-logger- failed for me, but cabal install monad-logger-0.3.0 I added this to the build-depends in the cabal file: monad-logger ==0.3.0.


So what did we do?

  1. Set up an isolated environment for our site to run in.
  2. Specified dependencies in todo.cabal.
  3. Wrote a basic scotty app with one route: /, and set it to run on port 3000.

Note that Scotty uses text instead of strings. Text is more efficient, but we don't want to keep converting strings to text:

import qualified Data.Text as T
html . T.pack $ "Hello World!"

So we need the OverloadedStrings pragma, which allows us to skip the call to T.pack.

The rest of this tutorial will be a terse look at Scotty's features.


Scotty supports GET, POST, PUT and DELETE requests:

get "/" $ do
  text "gotten!"

delete "/" $ do
  text "deleted!"

post "/" $ do
  text "posted!"

put "/" $ do
  text "put-ted!"

You can also match a route regardless of the method:

matchAny "/all" $ do
  text "matches all methods"

Or write a handler for when there is no matched route (this should be the last handler because it matches all routes):

notFound $ do
  text "there is no such route."

You can also specify named parameters. From the Scotty README:

get "/:word" $ do
  beam <- param "word"
  html $ mconcat ["<h1>Scotty, ", beam, " me up!</h1>"]

And unnamed parameters from a query string or a form too:

get "/hello" $ do
  name <- param "name"
  text name


Get a header:

get "/agent" $ do
  agent <- reqHeader "User-Agent"
  text agent

Set a header:

import Network.HTTP.Types

get "/adit" $ do
  status status302
  header "Location" ""

Content types

html "hello world"
text "hello world"
json ("hello world" :: String) -- you need types for json
json [(0::Int)..10]


blaze-html is a DSL that allows you to write html in Haskell. I like it because

  1. It gives you some type-checking of your html at compile time
  2. It gives you the full power of haskell while writing your templates instead of forcing you to learn a crippled templating language.

There's also a mustache implementation if you prefer.

To use blaze, first we need some qualified imports...blaze exports some functions which clash with Scotty or Prelude:

import Web.Scotty
import Text.Blaze.Html5
import Text.Blaze.Html5.Attributes
import qualified Web.Scotty as S
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

Next we import the necessary module to render blaze as text:

import Text.Blaze.Html.Renderer.Text

Here's our first app using blaze!

main = do
  scotty 3000 $ do
    get "/" $ do
      S.html . renderHtml $ do
        h1 "My todo list"

Writing html inline works fine since our "view" is just one line of html. In a real-world scenario, our views will be much bigger. My preferred workflow is to save the template in another file:

module Todo.Views.Index where

render = do
  html $ do
    body $ do
      h1 "My todo list"
      ul $ do
        li "learn haskell"
        li "make a website"

Then I define a function to render a blaze template, and import the view:

import qualified Todo.Views.Index
blaze = S.html . renderHtml

scotty 3000 $ do
  get "/" $ do
    blaze Todo.Views.Index.render

Logging Requests

By default the Scotty server won't log all the requests. You can do it with WAI middleware:

import Network.Wai.Middleware.RequestLogger

scotty 3000 $ do
  middleware logStdoutDev

Serving Static Content

Single files are easy:

get "/404" $ file "404.html"

For a directory of static files, Scotty uses middleware. Suppose you have a directory static in your root dir, with another directory called imgs inside static:

import Network.Wai.Middleware.Static

scotty 3000 $ do
  middleware $ staticPolicy (noDots >-> addBase "static")

Now we can add images to our site:

  get "/" $ do
    blaze $ do
      img ! src "/imgs/foo.png"

Note: the image src path doesn't include "static".


I use Persistent for models. It uses template haskell and can be tricky to use sometimes, but it's the best ORM I've found for Haskell so far.

Save this as Todo/Model.hs:

{-# LANGUAGE EmptyDataDecls       #-}
{-# LANGUAGE FlexibleContexts     #-}
{-# LANGUAGE FlexibleInstances    #-}
{-# LANGUAGE GADTs                #-}
{-# LANGUAGE OverloadedStrings    #-}
{-# LANGUAGE QuasiQuotes          #-}
{-# LANGUAGE TemplateHaskell      #-}
{-# LANGUAGE TypeFamilies         #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

module Todo.Model where
import Data.Text (Text)
import Data.Time (UTCTime)
import Database.Persist
import Database.Persist.TH

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persist|
    title String
    content Text
    createdAt UTCTime
    deriving Show

Here's a convenience function to write to the database (we're just using sqlite for now):

import Database.Persist
import Database.Persist.Sqlite

runDb :: SqlPersist (ResourceT IO) a -> IO a
runDb query = runResourceT . withSqliteConn "dev.sqlite3" . runSqlConn $ query

Now we can read and write from a database! Here's the full gist. First we run migrations if needed:

runDb $ runMigration migrateAll

Then we create posts using:

liftIO $ runDb $ insert $ Post _title "some content" now

And select posts using:

readPosts :: IO [Entity Post]
readPosts = (runDb $ selectList [] [LimitTo 10])

_posts <- liftIO readPosts
let posts = map (postTitle . entityVal) _posts

Deploying to Heroku

Deploying to Heroku is easy with the heroku buildpack.

First, our hello world app needs to change slightly. Heroku tells us what port to run on with the PORT env variable:

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import System.Environment
import Control.Monad

main = do
  port <- liftM read $ getEnv "PORT"
  scotty port $ do
    get "/" $ do
      html "Hello World!"

Then add a Procfile in your root dir to tell Heroku how to start your app:

web: ./dist/build/todo/todo

And a Setup.hs to build your app:

import Distribution.Simple
main = defaultMain

Then, assuming your project is a git repo:

heroku create --stack=cedar --buildpack \
git push heroku master

It will take ~15 minutes to build your project. Congratulations, you just deployed your Haskell site to Heroku!

If you're deploying to Heroku, you will probably want to use PostgreSQL instead of Sqlite, so change runDb to this:

import Database.Persist.Postgresql (withPostgresqlConn)
import Web.Heroku (dbConnParams)
import Data.Monoid ((<>))

runDb :: SqlPersist (ResourceT IO) a -> IO a
runDb query = do
    params <- dbConnParams
    let connStr = foldr (\(k,v) t ->
        t <> (encodeUtf8 $ k <> "=" <> v <> " ")) "" params
    runResourceT . withPostgresqlConn connStr $ runSqlConn query

And follow these steps to deploy to heroku:

heroku create --stack=cedar --buildpack \
heroku addons:add heroku-postgresql:dev
# just searches for the postgres db name so we can promote it
export dbname=$(heroku config | grep POSTGRES | cut -d: -f1)
heroku pg:promote $dbname
git push heroku master


You can test your Scotty app with wai-test. Here are some examples from the README:

spec :: Spec
spec = with app $ do
  describe "GET /" $ do
    it "reponds with 200" $ do
      get "/" `shouldRespondWith` 200

    it "reponds with 'hello'" $ do
      get "/" `shouldRespondWith` "hello"

Whats Missing

  1. Easy to use cookies. You can see an example of how to roll your own function to use cookies here. But I'd like to see this built into Scotty. Update: Another third-party solution is to use the wai-session middleware with clientsession.
  2. Asset Aggregation. Yesod and Snap both have their own asset aggregators, but afaik there's nothing like this for Scotty.
  3. Version management for modules. Sure, you can specify exact versions in todo.cabal...but if you don't, there's no way to guarantee that you'll run the same version on all machines. Bundler does this with Gemfile.lock, I wish Haskell had the same feature.


Privacy Policy