- Getting Set Up
- Hello World
- Routing
- Headers
- Content Types
- Templates
- Logging Requests
- Serving Static Content
- Databases
- Deploying To Heroku
- Testing
- Whats Missing
- References
Contents
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 for the backend
- Blaze-html for templating
- and Persistent for the ORM.
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
homepage: https://github.com/yourName/repoName
license: MIT
author: Aditya Bhargava
maintainer: you@email.com
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
.cabal-sandbox/bin/todo
Go to http://localhost:3000 and you should see your new Haskell site!
Troubleshooting
You might see a build failure with a cryptic message...something like:
cabal: Error: some packages failed to install:
monad-logger-0.3.3.0 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-0.3.3.0
. 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-0.3.3.0
failed for me, but cabal install monad-logger-0.3.0
succeeded...so I added this to the build-depends
in the cabal file: monad-logger ==0.3.0
.
Recap
So what did we do?
- Set up an isolated environment for our site to run in.
- Specified dependencies in
todo.cabal
. - Wrote a basic scotty app with one route:
/
, and set it to run on port3000
.
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.
Routing
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
Headers
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" "http://www.adit.io"
Content types
html "hello world"
text "hello world"
json ("hello world" :: String) -- you need types for json
json [(0::Int)..10]
Templates
blaze-html
is a DSL that allows you to write html in Haskell. I like it because
- It gives you some type-checking of your html at compile time
- 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".
Databases
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|
Post
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 \
https://github.com/pufuwozu/heroku-buildpack-haskell.git
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 \
https://github.com/pufuwozu/heroku-buildpack-haskell.git
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
Testing
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
- 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.
- Asset Aggregation. Yesod and Snap both have their own asset aggregators, but afaik there's nothing like this for Scotty.
- 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 withGemfile.lock
, I wish Haskell had the same feature.