This getting started tutorial will get you up and running with a basic web application. At the end you will know
- how install Simple
- how to create a Simple application from scratch
- the structure of a Simple application
- how to add persistence with PostgreSQL.
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
Open up a terminal. Commands prefaced with a dollar sign ($) should be run in terminal. Use cabal to install Simple:
[crayon-5ba4e8394603d876115493/] To verify that Simple installed properly run the following command:
[crayon-5ba4e83946046306481999/] which should print out the subcommands and options available for the
smplutility that comes with Simple.
Creating the Blog application
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”:
[crayon-5ba4e8394604a063980911/] 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:
|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.
[crayon-5ba4e8394604f384307729/] 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.
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:
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:
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:
routeTop $ render "index.html ()" with logic to read posts from the filesystem and and render them in the “index.html” template:
[crayon-5ba4e83946060035893614/] 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
transformers as dependencies in
blog.cabal (they should already be installed since Simple depends on them anyway):
[crayon-5ba4e83946064660131223/] Let’s now modify “views/index.html” to make use of the posts:
[crayon-5ba4e83946068454960630/] 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.
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):
and add a view template in “views/show.html”:
[crayon-5ba4e83946070779947951/] 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…
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:
[crayon-5ba4e83946073621020673/] 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”:
[crayon-5ba4e83946076137468204/] 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).
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
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:
[crayon-5ba4e8394607a201736411/] 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
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:
[crayon-5ba4e83946080110343789/] 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):
[crayon-5ba4e83946083681943327/] There are similar methods for
Once we import
Web.Frank, we can rewrite our application much more cleanly using this interface. The full listing is: