Getting Started Tutorial: Simple Basic Web Application

This getting started tutorial will get you up and running with a basic web application. At the end you will know

  1. how install Simple
  2. how to create a Simple application from scratch
  3. the structure of a Simple application
  4. how to add persistence with PostgreSQL.

Guide Assumptions

This guide assumes you have a working version of the GHC Haskell compiler, the cabal package manager and an up-to-date version of the PostgreSQL database. The best way to get GHC and cabal setup is by installing the Haskell platform. Most Linux distributions have PostgreSQL in their package repositories (e.g. apt-get install postgresql, pacman -S postgresql). Mac OS X comes with PostgreSQL, however, some of the utilities that this guide relies on (like pg_ctl) are not shipped by default. However, installing PostgreSQL from Homebrew will also install the appropriate utilities.
The guide also assumes you have a basic understand of Haskell programming, web programming and preferably have built a few web applications in the past. For a good starting guide to Haskell see Learn You a Haskell for Great Good!and/or Real World Haskell.

Creating a new Simple app

Installing Simple

Open up a terminal. Commands prefaced with a dollar sign ($) should be run in terminal. Use cabal to install Simple:

$ cabal install simple

To verify that Simple installed properly run the following command:

$ smpl --help

which should print out the subcommands and options available for the smplutility that comes with Simple.

Creating the Blog application

The smpl utility helps you create a new, blank Simple application from the command-line. To create our application, open a terminal, navigate to a folder where you would like to create the project (for example cd ~/hack) and create a new Simple project called “blog”:

$ smpl create --templates blog

This will create a new subdirectory called “blog” containing our app. The “–templates” flag tells smpl to include boilerplate code for templates. The directory contains a ready-to-run app with the following structure:

File/Folder Purpose
Blog/ Parent directory for app specific modules
Blog/Common.hs A base module that defines the application type
layouts/ The default folder for defining view templates
views/ The default folder for defining views
Application.hs Initial setup and route configuration
blog.cabal Used by cabal to resolve package dependencies and compile the app
Main.hs Contains the main function used to start the application

Starting the server

Our application is ready for us to get to work. Now we’ll get a server up and running and start adding functionality to our application.
Simple apps can be run using the warp web server (or any other WAIcompatible server). The generated Main.hs file does exactly this. The following commands will start a server on port 3000.

$ cabal install --only-dependencies
$ cabal run

To see the application in action, open a browser and navigate tohttp://localhost:3000/. You should see the default generated home page of Hello World.

Displaying Content

The default generated application isn’t very interesting, displaying only a boilerplate homepage. We’ll start by adding some content. For simplicity we can store blog posts in the filesystem. Let’s create some dummy data:

$ mkdir data
$ cat > data/00001
The Title of Your First Post on a Single Line
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Etiam vitae interdum sapien. In congue...
$ cat > data/00002
The Title of Your Second Post
Aliquam tempor varius justo vitae bibendum! Duis vitae rutrum
neque. Sed ut sed...

List posts

We’ll use the filename to order posts, the first line of the file for post title and the rest for the post body. Now that we have some data to play with, let’s list and display our posts.
For this simple tutorial, we’ll write all of our application logic in “Application.hs”. First, add the following imports:

import Control.Monad
import Control.Monad.IO.Class
import Data.Aeson
import Data.List
import Network.HTTP.Types
import System.Directory
import System.FilePath
import System.IO

Next, let’s modify the actual application in the app function. The first line in the function sets up the app settings – which is defined in Blog.Common. The rest of the function runs the HTTP server (runner) using the application defined in the block:

app runner = do
  settings <- newAppSettings
  runner $ controllerApp settings $ do
    routeTop $ render "index.html" ()

Replace routeTop $ render "index.html ()" with logic to read posts from the filesystem and and render them in the “index.html” template:

routeMethod GET $ do
  -- Respond to the root route
  routeTop $ do
    posts <- liftIO $ do
      dataDir <- getDirectoryContents "data"
      let postFiles = sort $
            filter (not . isPrefixOf ".") dataDir
      forM postFiles $ \postFile -> do
        withFile ("data" </> postFile) ReadMode $ \h -> do
          title <- hGetLine h
          return $ object ["id" .= postFile
                          , "title" .= title]
    render "index.html" $ object ["posts" .= posts]

The above code responds to a GET request for the root route (“/”), rendering the template “views/index.html” and passing it an JSON value (a aeson Value) containing a list of post objects. Each post contains an id and title. routeTopensures that the route is only invoked if there is no path remaining to consume. The rest of the code simply reads the first line (the title) of each file in the “data” directory.

We also need to add the packages aeson, directory, filepath, http-typesand transformers as dependencies in blog.cabal (they should already be installed since Simple depends on them anyway):

build-depends:
  base
  , aeson
  , directory
  , filepath
  , http-types
  , transformers
  , simple >= 0.7.0
  , wai
  , wai-extra
  , warp

Let’s now modify “views/index.html” to make use of the posts:

$if(null(posts))$
No posts.
$else$
<ul>
$for(post in posts)$
<li><a href="/$post.id$">$post.title$</a></li>
$endfor$
</ul>
$endif$

Simple templates are embedded templates – meaning they are embedded inside HTML, or whichever relevant output format. Template directives (like control statements and variable expansions) are surrounded by dollar signs ($). Our template lists the post titles and links to the post itself at “/:post.id”. For a comprehensive overview of the templating language, see the Haddock documentation.

Show post

If we click on any of the links now, we’ll get a 404 (not found) page. That’s because we still need to add the route for displaying individual posts. Let’s another route in “Application.hs” (make sure it’s as indented as the main route – two indentations):

-- Respond to "/:post_id"
routeVar "post_id" $ routeTop $ do
  postId <- queryParam' "post_id"
  let postFile = "data" </> (takeFileName postId)
  post <- liftIO $ do
    h <- openFile postFile ReadMode
    title <- hGetLine h
    body <- hGetContents h
    return $ object ["title" .= title, "body" .= body]
  render "show.html" post

and add a view template in “views/show.html”:

<h2>$title$</h2>
<p>
$body$
</p>

Now, if we click on a link from the main page, our app will display the post body:
We nearly have a complete (albeit minimal) blog application. We’re just missing a way to generate the content in the first place…

Creating content

New post form

Our first step towards being able to author new posts is to display an HTML form. This is fairly straight forward since it involves no dynamic content. We’ll add the route “/new” which will simply render the form:

-- Render new post form
routeName "new" $ routeTop $ do
  render "new.html" ()

It’s imperative that this route appears before the route for displaying posts. That’s because routes are evaluated in order, and routeVar "post_id" would match “/new”, which we don’t want.
Finally, we need to add a template in “views/new.html”:

<form action="/" method="POST">
  <p>
    <label for="title">Title</label>
    <input type="text" name="title" id="title"/>
  </p>
  <p>
    <textarea name="body"></textarea>
  </p>
  <p><input type="submit" value="Create"/>
</form>

Now, http://localhost:3000/new:

Parsing the form

Submitting the form above will perform a “POST” request to the root path with a URL-encoded body containing the contents of the form. In order to store the new post, we need to parse the form and ensure that the data is valid (i.e. the title and body fields are non-empty).
The monadic parseFrom function parses a form into a list of parameters (each a pair of strict ByteStrings for the key and value) and a list of FileInfos (FileInfo represents an uploaded file, but we won’t go into that now as it’s not relevant for our example).
parseForm lets us save new posts relatively easily:

...
import qualified Data.ByteString.Char8 as S8
...
-- Create form
routeMethod POST $ routeTop $ do
  (params, _) <- parseForm
  let notNull = not . S8.null
  let mpost = do
        title <- notNull `mfilter` lookup "title" params
        body <- notNull `mfilter` lookup "body" params
        return (title, body)
  case mpost of
    Nothing -> redirectBack
    Just (title, body) -> liftIO $ do
      files <- filter (\(c:_) -> c /= '.') `fmap`
        getDirectoryContents "data"
      let lastFileNum = show $ length files + 1
      let fileName =
            take (5 - length lastFileNum)
              [z | _ <- [0..], let z = '0'] ++
            lastFileNum
      withFile ("data" </> fileName) WriteMode $ \h -> do
        S8.hPutStrLn h title
        S8.hPutStr h body
  respond $ redirectTo "/"

Once we’ve extracted the parameters from the request body, we lookup the “title” and “body” fields (note that these just correspond to the “name” attribute we gave the inputs in our HTML form) and ensure they are not empty (with notNull and `mfilter). If this fails (i.e. if “title” or “body” are either not present or empty), we redirect to the referrer (the new post form). In a real application, we’d probably want to give the user some hint as to what went wrong. If the form is complete, we store the post and redirect to the post listings.

We’re basically done! Our blog app, while very simple, is totally functional!

Bonus! Routing DSLs

The route* combinators are very expressive and are, therefore, great for customizing exactly how to route a request. However, in the common case, where an application follows a simple pattern, they can get a bit cumbersome to use. Simple ships with two DSLs on top of the route* combinators that make common routing tasks easy. Let’s use one of these DSLs, “Frank”, to rewrite out blog application more concisely.
“Frank” exposes an interface based on the Sinatra framework for Ruby. For example, the route:

get "/:post_id" $ do
  ...

will match GET requests which have exactly one unconsumed directory in the path and use its contents for the “post_id” query parameters. The route is equivalent to (and in fact implemented as):

routeMethod GET $ routePattern "/:post_id" $ routeTop

There are similar methods for post, put and delete.
Once we import Web.Frank, we can rewrite our application much more cleanly using this interface. The full listing is:

{-# LANGUAGE OverloadedStrings #-}
module Application where
import Control.Monad
import Control.Monad.IO.Class
import Blog.Common
import Data.Aeson
import qualified Data.ByteString.Char8 as S8
import Data.List
import Network.HTTP.Types
import System.Directory
import System.FilePath
import System.IO
import Web.Frank
import Web.Simple
import Web.Simple.Templates
app :: (Application -> IO ()) -> IO ()
app runner = do
  settings <- newAppSettings
  runner $ controllerApp settings $ do
    get "/" $ do
      posts <- liftIO $ do
        dataDir <- getDirectoryContents "data"
        let postFiles = sort $
              filter (not . isPrefixOf ".") dataDir
        forM postFiles $ \postFile -> do
          withFile ("data" </> postFile) ReadMode $ \h -> do
            title <- hGetLine h
            return $ object ["id" .= postFile
                            , "title" .= title]
      render "index.html" $ object ["posts" .= posts]
    -- Respond to "/new"
    get "/new" $ do
      render "new.html" ()
    -- Respond to "/:post_id"
    get "/:post_id" $ routeTop $ do
      postId <- queryParam' "post_id"
      let postFile = "data" </> (takeFileName postId)
      post <- liftIO $ do
        h <- openFile postFile ReadMode
        title <- hGetLine h
        body <- hGetContents h
        return $ object ["title" .= title, "body" .= body]
      render "show.html" post
    -- Create form
    post "/" $ do
      (params, _) <- parseForm
      let notNull = not . S8.null
      let mpost = do
            title <- notNull `mfilter` lookup "title" params
            body <- notNull `mfilter` lookup "body" params
            return (title, body)
      case mpost of
        Nothing -> redirectBack
        Just (title, body) -> liftIO $ do
          files <- filter (\(c:_) -> c /= '.') `fmap`
            getDirectoryContents "data"
          let lastFileNum = show $ length files + 1
          let fileName =
                take (5 - length lastFileNum)
                  [z | _ <- [0..], let z = '0'] ++
                lastFileNum
          withFile ("data" </> fileName) WriteMode $ \h -> do
            S8.hPutStrLn h title
            S8.hPutStr h body
      respond $ redirectTo "/"

Aly Chiman

Aly Chiman is a Blogger & Reporter at AlyChiTech.com which covers a wide variety of topics from local news from digital world fashion and beauty . AlyChiTech covers the top notch content from the around the world covering a wide variety of topics. Aly is currently studying BS Mass Communication at University.