Building a Blog with Servant and Opaleye, Part 1 - Setting Up the API

Posted on August 30, 2016
Go to comments.

In the next series of blog posts, I’ll be working through setting up a simple blog backend in Haskell using Servant for the server and Opaleye to manage database queries. Aside from working through those two libraries, this is intended to be a beginner-intermediate tutorial, aimed at someone who has become familiar with the idea of monads and monad transformers (for example) but who would like a little bit of help in applying them to a real-world project.

For further information, you may want to check out the Servant Tutorial and the Opaleye Basic Tutorial. You do not necessarily need to fully understand those tutorials in order to read this one; I wrote the current one myself to figure out what was what.

Note: This tutorial has been tested with Servant 0.8. I’ll try to make sure to come back every once in a while and update it to the current version as necessary.

Step 0: Set It Up

Here, we’ll set up a very basic Servant API. It won’t do much; our GET requests will return hardcoded data, and our POST requests will just append the posted data to the hardcoded response. But we will be able to use almost all of the API functionality as we go forward.

If you want to create the project on your own, you can type stack new blog-tutorial servant at the command line.

Code for lesson 1 can be found at: https://github.com/nomicflux/servant-opaleye-blog/tree/lesson1_servant_api. (Note: if you do clone the code, make sure to git checkout lesson1_servant_api to get to the correct branch.)

Step 1: Create App.hs

This is the file where I put basic information for the entire application. For example, I get tired of typing ExceptT ServantErr IO all of the time, so I’ve created a type alias AppM instead. Once we get to Lesson 4, we’ll see that this setup will greatly ease our transition into more complex transformers.

Similarly, I’ve created typealiases for BlogPostId and Email as well. In theory, the rest of the code should just have to know that it is dealing with emails and ids, and not worry about the underlying representation. It is, of course, more complicated than that, since we’ll also have to connect up Haskell’s representation with the database and with JSON inputs, but this is a start toward full modularity.

Step 2: Create API directory

The default setup provided by Stack places all of the API information in the Lib.hs file. That’s nice for a quick and dirty website, but we want to get maximum reuse out of our components. We might be writing other websites which deal with users, for example - it happens from time to time. So let’s create a directory just for API files, and we’ll get to work writing API/User.hs and API/BlogPost.hs.

Step 3: Set up User API

Datatype

To start, take the User information which Stack graciously provided in Lib.hs, and move it to it’s own file. We’ll rename some things: API becomes UserAPI, server becomes userServer.

To keep things simple, we will have just two fields for our User: an email (which will double as a unique identifier) and a password. We’ve already set up the Email type alias in the App.hs file; we’ll want to refer to Emails from the BlogPost API file as well, but we don’t necessarily want BlogPosts to be dependent on our implementation of User beyond this one detail. We end up with this:

JSON

Next, let’s create JSON representations. We will use a different representation when converting to JSON as opposed to converting from JSON, so we’ll have to roll our own toJSON and parseJSON functions. When converting toJSON, we’ll just package up the userEmail field - we should never return user passwords!

However, when using parseJSON, we’ll take in both a userEmail and a userPassword:

If you have not used AESON before to convert to/from JSON, what we are doing is this: in toJSON, we set up an object, which matches up JSON keys with whatever we want. Here, I lined up the key “email” with userEmail user, but I could have just set all emails to “bob@juno.com” if I felt like it.

To convert from JSON, we set up a parseJSON function, which takes an object and parses out the fields using .:. So, for example, object .: "email" is something like javascript_object.email in javascript, which can then be used as part of a User datatype. The main gotcha to watch out for is that AESON uses Text instead of String, so we have to add {-# LANGUAGE OverloadedStrings #-} if we don’t feel like manually packing each String.

API

What good is an API without the API itself? We’ll set up the endpoints as such:

We’ll have a root endpoint which responds to a GET and returns a List of Users in JSON format. Next, we’ll add an endpoint at “/{someone’s email}” which will look up our current users and Maybe return one. Finally, we’ll let people POST a User to our mini-server, and which will return a list of Users, also in JSON.

Then, we’ll make sure to add a Proxy for our API:

This is a little bit of boilerplate which lets the Type system interact with values which we pass around. Basically, we can’t send UserAPI, the Type, as an argument to anything, since it’s not a value. So instead, we send a Proxy in its place: userAPI. Don’t worry about it too much; by the time you’ll need to do anything like this in your code (if ever), everything will be clear and you’ll be writing the tutorials.

Server

Above, we’ve set out our API. We have our endpoints, what they expect from us, and what we expect from them. This is really just setting up type signatures, however. We’ll need to implement a server to make those endpoints do something:

Make sure that the server is in the same order as the API, otherwise the compiler will yell at you.

And if you give an API a Server, it will want some functions. So let’s set up some basic functions for the server as well:

As you’ll notice, we are using AppM in our return value. This was defined in App.hs as ExceptT ServantErr IO. If you wanted to, you could type that in directly, and end up with type signatures such as User -> ExceptT ServantErr IO [User]. But, a) that is a pain to read and type, and b) we’ll be changing it in a future lesson, so abstracting the type out to AppM now will save time.

Step 4: Set up BlogPost API

Repeat the above steps to now set up a blog post. Create the file “API/BlogPost.hs”. For a datatype, we’ll use:

All of the steps will be the same as for the User API. For an exercise, see how much you can implement on your own without looking at the BlogPost.hs file in the “src/API/” directory.

Step 5: Put the APIs together

Now we have a User API and a BlogPost API, with their respective servers. We’ll need to put them together for our main application. Let’s go back to Lib.hs, and add to the import list:

Forming an API out of sub-APIs is no different than what we’ve already done:

We create the type by gluing together the sub-APIs and their respective endpoints. Then we make a proxy and set up a server. The server just refers back to the userServer and the blogPostServer we’ve already created.

Finally, we need to create an application to do the actual serving. This should already be part of the Lib.hs code:

Step 6: Edit Cabal File

Ok, so the code is written, time to fire this thing up, right? Not so fast - you could end up with some nasty, uninformative error messages at this point.

First, make sure that you add the modules we’ve been working on to the Cabal file. These will be App (for the App.hs file holding cross-module information), Api.User, and Api.BlogPost, in addition to Lib which Stack has started you off with. Put these in the exposed-modules section.

Second, add in all the depencies we’ve used. If you forget one, Cabal will let you know. Check the blog-tutorial.cabal file for the specific ones I’ve added.

Step 7: Run

At this point, you should have a working Servant server. Type stack build to build it, and stack exec blog-tutorial-exe to run it.

To test it out, try some basic curl commands:

(continued in Part 2)


Return to post.