Making A Website With Haskell

Written April 15, 2013

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 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.

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 with Gemfile.lock , I wish Haskell had the same feature.

References