Tutorial: Building a Sample Application with Haskell Snap, PostgreSQL, and the PostgreSQL Simple Snaplet

If you’re reading this, you probably don’t need to be sold on using Haskell or Postgres, so I’ll cut to the chase. Instead of telling you why both are astonishingly and almost painfully amazing—and here at Janrain we use Haskell, the Snap web framework, and PostgreSQL (as well as the PostgreSQL Simple module itself) to power our Janrain Capture product, so we can vouch for that—I’ll skip straight to the tutorial.

What you’ll find below is more of a streamlined presentation of a lot of disparate sources (for example here and here) than a 100% new tutorial, but I hope that it serves to put in easily digestible narrative form a lot of the currently existing information. I’ll be putting together an extremely simple product management tool (basically the most barebones and useful Basecamp clone out there). I will do nothing on the UI level and will interact with the server only via cURL. I’ll save views and fancy aesthetics for another day.

Get a Snap Application Up and Going

Since this tutorial also serves as a rudimentary introduction to Snap for the uninitated, I’ll briefly walk through starting a new Snap application. If you’ve installed Haskell, cabal, and Snap, simply create a new project directory (we’ll call our project projectomatic), navigate to the directory, and initiate a new Snap application:

mkdir projectomatic
cd projectomatic
snap init

Once we’ve done that, if we run ll or ls or whatever, we’ll see that a variety of files and directories have been created. I won’t delve too deeply into that here, so I recommend reading up here if you want to learn more about the anatomy of a Snap application. I could have run snap init barebones to begin with a much more stripped-down application, but I’m going to go for a full installation so that we can more quickly get up to speed on using Snaplets.

Setting up Our Application to Use PostgreSQL simple

Snaplets are somewhat difficult to explain, but I’ll try to encapsulate it briefly. Snaplets are sort of a portal to the outside world from within a Snap application. Snaplets enable you to access state—database transactions being a perfect example of that—in a relatively painless fashion. Setting up our application to use PostgreSQL Simple involves using the very handy PostgreSQL Simple Snaplet. First, install that using cabal (cabal install snaplet-postgresql-simple), then add that to your dependencies in your projectomatic.cabal file, run cabal install, then modify your src/Application.hs file like so:

{-# LANGUAGE FlexibleInstances #-}

import Control.Lens
import Snap (get)
import Snap.Snaplet
import Snap.Snaplet.PostgresqlSimple

data App = App
    { _pg :: Snaplet Postgres }

makeLenses ''App

instance HasPostgres (Handler b App) where
    getPostgresState = with pg get

With the normal snap init install, your application is set up to use a few other Snaplets out of the box. Feel free to leave those in. I will remove them for the sake of brevity in this tutorial. What you see above is that I’ve embedded our Postgres Snaplet within our App data type and then used the makeLenses ''App function to generate an accessor pg that we can use elsewhere. Lastly, I’ll define the instance HasPostgres (Handler b App), which will enable the application to have access to Postgres-related state.

Now that we have our handy pg accessor, we need to go over to our src/Site.hs file and do some additional setup. First, add the PostgreSQL Snaplet to the imports:

import Snap.Snaplet.PostgresqlSimple

Now, we need to add our pg constructor to the core of the application. Let’s modify the definition of the app function to include our Postgres Snaplet:

app :: SnapletInit App App
app = makeSnaplet "app" "My stunningly advanced Snap application." Nothing $ do
    pg <- nestSnaplet "pg" pg pgsInit
    addRoutes routes
    return $ App pg

We’re almost ready. All we have to do now is configure our Postgres connection. When we ran cabal install after adding the snaplet-postgresql-simple module to our dependencies, Snap created a snaplets/postgresql-simple directory on its own (wasn’t that sweet?). In that directory, there’s a file called devel.cfg where we specify our database connection. Here’s what mine will look like (yours will vary, of course!):

host = "localhost"
port = 5432
user = "luc"
pass = ""
db = "projectomatic"

There are other configuration possibilities, but this will be enough to get me up and going. Once this is done, try running cabal install and see if you get any compiler errors. If so, see if you can debug on your own. If not, then let’s move on to the next section.

Our Project Data Type

Haskell is a real stickler for data types, so we now have to start being both explicit and careful about everything we do. Let’s define our Project data type in our src/Site.hs file. We could do it in a separate file if we wanted to, but let’s keep it simple. I’ll be using the Text data type here—as use of strings is widely considered to be deprecated in Haskell)—so I’ll make sure and add Data.Text to my imports:

import qualified Data.Text as T

We’ll keep our data type simple:

data Project = Project
  { title       :: T.Text
  , description :: T.Text
  }

Setting up Postgres to Handle Our Data

Postgres is a traditional columnar database (though with a of sugar on top), so we can’t simply start throwing a bunch of key/value pairs at it and expect it to start storing things. We’ll need to create tables that are ready to do our bidding. Oh, and our database. We should make one of those, too. Let’s start with that. In a *nix environment, chances are strong that you can create a new database from the command line (presuming that you have Postgres installed):

createdb projectomatic

If that doesn’t work, either consult one of many Postgres installation tutorials or cut to the chase and download Postgres.app for real ease of use. If that did work, then let’s write a projectomatic.sql file and place it in our main project directory. Here’s what it will need to look like:

CREATE TABLE projects (
  title TEXT NOT NULL,
  description TEXT NOT NULL,
);

Each project will simply have an integer identifier, a title, a description, and a set of users involved with the project. For sake of simplicity, we’ll set things up so that each user will simply have a username and a set of projects that they’re associated with, and none of the values we’re working with can be NULL.

Let’s open up our database using psql projectomatic in the command line and run our SQL script when we get there:

i /path/to/application/dir/projectomatic.sql

If we get CREATE TABLE as a response, then we’re probably good to go. It’s a good idea to double check by running dt and making sure that projects and users tables currently exist and are properly constructed. Once they do, we should be good to go within Postgres itself.

Making Our Data Types Ready for Postgres

Like I said before, dealing with data types in Haskell is tough. While I would never assert that dealing with data types in any programming language is “easy” per se, Haskell really does present a pretty formidable learning curve. Fortunately, it’s not too bad if you’re doing fairly basic things.

Previously, we specified our Project data type, but that’s not yet enough to get them to actually interact with Postgres. Putting data into Postgres would likely work fine at this stage, but taking data out of Postgres and transforming it into something that Haskell can use within the application is going to take some work.

So in our Site.hs file, we need to set up a FromRow instance of both of our main data type. This will require the Control.Applicative module as well as a special submodule of PostgreSQL Simple, namely FromRow. Here’s what needs to be added to our Site.hs file:

import Control.Applicative
import Database.PostgreSQL.Simple.FromRow

instance FromRow Project where
    fromRow = Project <$> field <*> field

The funny <$> and <*> operators come from the Control.Applicative module. The most important thing is that the number of fields that you use when constructing the instances matches the number of fields in the database, or else you’ll get all kinds of nasty compiler errors, just as you will if there’s a type mismatch (as when you try, for example, to put a Haskell integer into a Postgres field that takes only a varchar or text).

Setting up these FromRow instance constructors enables us to take data from Postgres (for example the results of a SELECT * FROM projects-style query) and use it in our pure, pristine Haskell application. One thing we should also do is to define a Show instance for our data type. This is necessary because when we make an HTTP request to see a Project (or a list thereof), we want to specify what that will actually look like in the command line. If we don’t specify a Show instance, that means that we won’t actually see anything, even if we’ve done everything else right. I’ll keep it basic:

instance Show Project where
    show (Project title description) =
      "Project { title: " ++ T.unpack title ++ ", description: " ++ T.unpack description ++ " }n"

Note that we need to use the T.unpack function, taken from the Data.Text module, to convert text into strings (else we get a compiler error).

Time to Get Back to Our Application

So that was a lot of setup, no doubt about it. Now it’s time to actually convert HTTP requests into PostgreSQL queries and consequently data. I’m going to set up our server to take requests that do the following things:

  1. Create a new project
  2. Return a list of all projects
  3. Delete a project

Basically, we’re running simple CRUD operations on the Project data type.

Let’s start with creating a new project. First, our route:

("/project/new", method POST createNewProject)

Here’s our corresponding function:

createNewProject :: Handler App App ()
createNewProject = do
  title <- getPostParam "title"
  description <- getPostParam "description"
  newProject <- execute "INSERT INTO projects VALUES (?, ?)" (title, description)
  redirect "/"

Let’s unpack this a bit. First, notice that this is embedded in a do block, which often (though certainly not always) means that you’ll be dealing with I/O. The first thing that this function does is extract the title of our new project out of the form data passed to the server with the getPostParam function (the same goes for our description). Now, we’ll use the execute function to actually interact with Postgres. In contrast to the query_ function that we will encounter later, execute is typically used in situations where you don’t expect a return value (as when you write to a table).

The dual ?s that you see are places in our SQL bytestring where we will insert values. Immediately after the bytestring, we specify which values we want inserted (in this case the title and the description that we derived from the URL).

Now, let’s work on using the query_ function instead of execute. This is a bit trickier because it involves getting data back from Postgres that then needs to be processed and handled on the application side (whereas with the execute function we passed already-formed data on to Postgres). We’ll start with setting up a route that displays all projects. The route:

("/projects", method GET getAllProjects)

And the handling function:

getAllProjects :: Handler App App ()
getAllProjects = do
  allProjects <- query_ "SELECT * FROM projects"
  liftIO $ print (allProjects :: [Project])

A few things to notice here. First, we store the results of the query in the allProjects variable. That’s pretty straightforward. What’s more difficult, however, is actually displaying those projects. First, have a look at the print function. What is being printed? The allProjects variable is going to be printed as a list of projects. But we can’t just use print on its own because print produces IO (which is always tricky in Haskell).

This is where the liftIO function comes in handy. This function gives us access to the I/O in which the application is engaging, and in our case it will convert the type IO () into the type Handler App App (). And so print is producing IO which is then brought into the application context. Because we haven’t set up any views or anything fancy for displaying the data, the print function will make the results of our GET request show up in the console.

So now we can create a project and get all projects. Let’s add a route and a function for deleting specific projects. First, let’s set up a route to delete a project based on the project’s title:

("/project", method DELETE deleteProjectByTitle)

In a serious project, you would probably want to delete on the basis of an integer project ID or something of the sort, but we’ll keep it simple. Now, our corresponding function:

deleteProjectByTitle :: Handler App App ()
deleteProjectByTitle = do
  title <- getPostParam "title"
  deleteProject <- execute "DELETE FROM projects WHERE title = ?" (Only title)
  redirect "/"

Notice here that we’re only passing one value into our SQL bytestring, hence our reliance on the Only instance. Also notice that we’re not passing the project title via URL, but rather by way of form data.

So now we have some routes that interact with Postgres and do some really basic things. Let’s put our server to the test.

cURLing Our Way to Victory

As mentioned before, I’ve said nothing about views or HTML rendering or any of the sort. At the moment, our application has zero views. I’ll cover Snap’s default templating system, Heist in a future tutorial. For now, though, we’ll interact with our server the old-fashioned way: via cURL (my favorite tutorial can be found here).

Let’s go through a set of examples involving each of the routes that I set up earlier. First, let’s fire up our server.

cabal install
projectomatic -p 3000

Now, let’s get a list of all of our projects:

curl -X GET http://localhost:3000/projects

At first, this should return an empty list (no surprises there). So let’s create a project. Let’s create a project called “Getting in shape” and give it the description “Daily exercise:”

curl -X POST http://localhost:3000/project/new 
-d 'title=Getting in shape' 
-d 'description=Daily exercise'

Now, when we run a GET request on /projects again, we should get the following in the CLI:

[Project { title: Getting in shape, description: Daily exercise}
]

This means that our server works. *whew*

Now, let’s delete the project that we just added to the database. Remember from above that the URL takes the form /project, while our title is passed in via form data. Our cURL request should look like this:

curl -X DELETE http://localhost:3000/project 
-d 'title=Getting in shape'

If we have more than one project in the database with the title “Get in shape,” then all projects with this title will be deleted. This is yet another reason why working with integer IDs for important data types is much better than working in terms of attributes like this. In a future tutorial, I’ll do a deeper dive into Snap and Postgres that does precisely this.

Just a Basic Intro

Like I said before, this application has no views or rendering, no error handling, no authentication. It’s just an HTTP server talking to Postgres. I’ve also only covered a minimal smattering of what the PostgreSQL Simple Snaplet has on offer. But I hope that it has at least served to show that using Postgres and Haskell in a web development context is not as intimidating as it may seem at first. There are some conceptual leaps (at least for a previously OOP-oriented person like myself), but patience pays off.

11 comments
vushu
vushu

Im using windows 8 and my workaround is just copying the devel.cfg from C:\Users\Administrator\AppData\Roaming\cabal\snaplet-postgresql-simple-0.4.1\resources\db,

 

and using with db example :

 createNewProject :: Handler App App ()createNewProject = do  title <- getPostParam "title"  description <- getPostParam "description"  newProject <- with db $ execute "INSERT INTO projects VALUES (?, ?)" (title, description)  redirect "/"

 

and remove the line

instance HasPostgres (Handler b App) where    getPostgresState = with db get

 

Thx for this awesome tutorial :)

vushu
vushu

Does this example still work?

and im not getting any devel.cfg file in my postgres-simple folder, im not 

im getting this error when  doing this

instance HasPostgres (Handler b App) where    getPostgresState = with db get

 

error :

Illegal instance declaration for `HasPostgres (Handler b App)'       (All instance types must be of the form (T a1 ... an)        where a1 ... an are *distinct type variables*,        and each type variable appears at most once in the instance head.        Use -XFlexibleInstances if you want to disable this.)     In the instance declaration for `HasPostgres (Handler b App)'

mightybyte
mightybyte

Small correction:  

 

"When we rancabal install after adding the snaplet-postgresql-simple module to our dependencies, Snap created a snaplets/postgresql-simple directory on its own"

 

This isn't quite correct.  The snaplets/postgresql-simple directory is not created when you run cabal install.  It gets created when you run the application for the first time with the postgresql-simple snaplet.

liamoc
liamoc

Do notation has nothing to do with I/O, I/O is just one example of the uses of monads. <*> and <$> are not constructors.

What challenges does a POST request present? 

 

 

lucperkins
lucperkins

 @vushu To fix that, you'll want to add the following at the top of the file in which you declare the HasPostgres instance: {-# LANGUAGE FlexibleInstances #-}

lucperkins
lucperkins

 @mightybyte Good call on that. I'll fix it. Also, your comment reminds me that I need to give you credit for helping me out on IRC!

lucperkins
lucperkins

 @liamoc As you can see, I'm a bit of a Haskell greenhorn. Thanks for pointing all of the above out. I'll make the necessary changes.

 

As for POST requests, I've come to discover that the problem isn't with Snap but rather that cURL (at least the version I'm using) requires you to specify "Content-Length" along with any POST request. The workaround that I've discovered is including a `-d ""` flag, but this strikes me as being more than a little bit hacky. Do you know of a better way?

mightybyte
mightybyte

 @lucperkins  Ahhh, perhaps you could change your wording to clarify that?  It makes it sound like Snap has some scary problems when it's just simply using "method POST" instead of "method GET".