Building a REST API

Overview

In this tutorial, we’re going to use Spock to build a simple RESTful API that will let us add people to a database and then retrieve a list of who we’ve added. For example:

$ curl -H "Content-Type: application/json" -d '{ "name": "Walter", "age": 50 }' localhost:8080/people
{"result":"success","id":1}

$ curl -H "Content-Type: application/json" -d '{ "name": "Jesse", "age": 22 }' localhost:8080/people
{"result":"success","id":2}

$ curl localhost:8080/people
[{"age":50,"name":"Walter","id":1},{"age":22,"name":"Jesse","id":2}]

We’ll be using curl to interact with our API so you should have that or another way to perform HTTP requests. curl examples are provided throughout the tutorial.

You can find the finished code here. “part1” covers up to Adding a Database; “part2” covers up to Finishing up.

Project Setup

Before using Spock, you’ll need to install the Haskell toolchain on your machine. We recommend using the stack tool to quickly get started! Our guide will be stack based, but you can easily translate this to cabal. Next, you can prepare a directory for your first Spock powered application:

  1. Create a new project using stack new Spock-rest
  2. Jump into the directory cd Spock-rest

Dependencies

To make sure your dependencies will match those used in this tutorial, set your project to use Stackage LTS 16.18: Open your stack.yaml file, find the resolver key and make sure it is configured correctly:

resolver:
  url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/18.yaml

Next, you’re going to add the packages you’ll be using: Spock, aeson and text. To do this, open your Spock-rest.cabal file, find the executable Spock-rest-exe section and add aeson, Spock and text to the build-depends key. There should also be a Spock-rest entry which you can remove or just ignore. The result should look something like this:

  build-depends:         base
                       , aeson
                       , Spock
                       , text

During the first build via stack build --fast --pedantic, stack may ask you to add further entries to extra-deps. Follow these instructions.

Imports

Let’s start by adding a couple of language extensions and imports. Open app/Main.hs and replace the content with:

{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}

import           Web.Spock
import           Web.Spock.Config

import           Data.Aeson       hiding (json)
import           Data.Monoid      ((<>))
import           Data.Text        (Text, pack)
import           GHC.Generics

We’ll be using the DeriveGeneric extension along with GHC.Generics to create FromJSON and ToJSON instances of our API type. The Data.Aeson library provides our JSON type conversion; you can view its documentation to learn more.

API Type

Now we’ll create a data type for our API. Add the following to your Main.hs, below the import statements:

data Person = Person
  { name :: Text
  , age  :: Int
  } deriving (Generic, Show)

instance ToJSON Person

instance FromJSON Person

We can try out our new Person type in GHCi: run stack ghci in your project root directory to build your project and start a REPL session. Try using encode and decode to serialise and deserialise Persons into ByteStrings like so:

λ> encode Person { name = "Leela", age = 25 }
"{\"age\":25,\"name\":\"Leela\"}"

-- So our string literal can inferred as a ByteString
λ> :set -XOverloadedStrings
λ> decode "{ \"name\": \"Amy\", \"age\": 30 }" :: Maybe Person
Just (Person {name = "Amy", age = 30}) 

Aeson’s decode type signature is:

FromJSON a => ByteString -> Maybe a

So we have to tell GHCi what we want type we want it to try decode our bytestring as, otherwise it will default to (). We can also decode it as Aeson’s generic Value type:

λ> decode "{ \"name\": \"Amy\", \"age\": 30 }" :: Maybe Value
Just (Object (fromList [("age",Number 30.0),("name",String "Amy")]))

Serving JSON

Now we’ll add a basic Spock application to serve our JSON. Add the following to your Main.hs:

type Api = SpockM () () () ()

type ApiAction a = SpockAction () () () a

main :: IO ()
main = do
  spockCfg <- defaultSpockCfg () PCNoDatabase ()
  runSpock 8080 (spock spockCfg app)

app :: Api
app = do
  get "people" $ do
    json $ Person { name = "Fry", age = 25 }

Our Api type represents our application’s configuration. In the second part we’ll be modifying it to add a database backend, but for now we’ll leave all the types as Unit. Our ApiAction type is similar and represents actions in our application which are functions performed by route matches (e.g. get "people"). We’ll be using ApiAction later to explicitly declare some types used in actions.

Spock includes a json function for serving any type that implements the ToJSON typeclass, which means you can pass your Person and it will encode it as JSON and set the HTTP Content-Type header to application/json for you. You can start the server in GHCi by first reloading the project using the :reload command then running your main function:

λ> :reload
[2 of 2] Compiling Main
Ok, modules loaded: Lib, Main.

λ> main
Spock is running on port 8080 

Go to localhost:8080/people and you should see your Person object in JSON.

REST APIs also need to serve lists of items; since aeson includes a ToJSON a => ToJSON [a] instance, we can easily serve a list of Persons. Change your get function to the following:

  get "people" $ do
    json [Person { name = "Fry", age = 25 }, Person { name = "Bender", age = 4 }]

Enter ctrl-c in GHCi to interrupt the server and get back to the prompt. Then, reload your project and start the server again. Refresh your browser to see your two Persons in a JSON array.

Parsing JSON

Now we’ll write a second route that will attempt to parse a POST body into our Person type. Add the following to the bottom of your app declaration:

  post "people" $ do
    thePerson <- jsonBody' :: ApiAction Person
    text $ "Parsed: " <> pack (show thePerson)
      

Reload your project and start the server again. We’ll need to make a POST request to try out our new code. Here’s a way of doing so using curl:

$ curl -H "Content-Type: application/json" -d '{ "name": "Bart", "age": 10 }' localhost:8080/people
Parsed: Person {name = "Bart", age = 10}

You can also try adding extra keys to your JSON object or removing name and age and seeing what happens.

Adding a Database

Now that we’ve seen how to do some simple REST-style requests, we’ll add a database to our application so we can provide proper API functionality. We’re going to be using the Persistent library and SQLite. The Persistent chapter in the Yesod Book covers a lot of what we’ll be using Persistent for and much more, so make sure to refer to it while following this section.

To use Persistent, we’re first going to add its dependencies to our cabal file. Add these entries to your build-depends key:

                     , monad-logger
                     , persistent
                     , persistent-sqlite
                     , persistent-template

You’ll have to restart GHCi so it can build these new dependencies. Exit GHCi with :quit and run stack ghci again. Stack should rebuild your project and show you the GHCi prompt.

We’ll have to add quite a few more language extensions and imports. At the top of your Main.hs, add the following lines below your current extensions:

{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE FlexibleInstances          #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}

And these imports below your existing ones:

import           Control.Monad.Logger    (LoggingT, runStdoutLoggingT)
import           Database.Persist        hiding (get) -- To avoid a naming clash with Web.Spock.get
import qualified Database.Persist        as P         -- We'll be using P.get later for GET /people/<id>.
import           Database.Persist.Sqlite hiding (get)
import           Database.Persist.TH

Next, we’re going to replace our existing Person declaration with a Persistent-specific one. This new one will generate code at compile time that will allow us to serialise our datatype to SQLite with ease. Find and remove your data Person = ... declaration along with instance FromJSON and instance ToJSON and replace them with the following:

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person json -- The json keyword will make Persistent generate sensible ToJSON and FromJSON instances for us.
  name Text
  age Int
  deriving Show
|]

This code will (among other things) generate record names for Person that are slightly different to our initial ones: name becomes personName and age becomes personAge. If you change your Person definitions in your get function accordingly:

    json
      [ Person {personName = "Fry", personAge = 25}
      , Person {personName = "Bender", personAge = 4}
      ]
      

Then your code should still compile and behave similarly to how it did before the change.

Creating the Connection Pool

Spock’s configuration type includes a database helper that manages connection pools. Let’s create a SQLite connection pool and add it to our Spock configuration. Replace the spockCfg assignment (the spockCfg <- ... line) in your main function with the following:

  pool <- runStdoutLoggingT $ createSqlitePool "api.db" 5
  spockCfg <- defaultSpockCfg () (PCPool pool) ()

If we look at createSqlitePool’s type signature, we can see the MonadLogger m constraint. This means that createSqlitePool needs to be in a monad that provides logging, so we run it inside runStdoutLoggingT which will print the function’s log messages to standard output, allowing us to see its debug messages in our console.

Because we’ve changed the structure of our application to contain a connection pool, we’ll have to change our Spock application’s types to reflect this. Change your Api type:

type Api = SpockM SqlBackend () () ()

and your ApiAction type:

type ApiAction a = SpockAction SqlBackend () () a

Creating the Schema

Our new Person declaration also includes functionality for Persistent to migrate our schema. We’ll call Persistent’s runMigration function which will create our schema for us. Insert this line right below the spockCfg definition:

  runStdoutLoggingT $ runSqlPool (do runMigration migrateAll) pool

The runSqlPool function, which also needs to be in a MonadLogger monad, takes two arguments: a function that performs actions using a connection from a pool (which here is wrapped in parentheses), and the pool itself.

Running Queries

We’ll use a small helper function for running queries in our server. Add the following function to your Main.hs:

runSQL
  :: (HasSpock m, SpockConn m ~ SqlBackend)
  => SqlPersistT (LoggingT IO) a -> m a
runSQL action = runQuery $ \conn -> runStdoutLoggingT $ runSqlConn action conn

If you compare the right side of the runSQL definition to the migration code in the last section, you’ll see some similarities:

  runQuery $ \conn -> runStdoutLoggingT $ runSqlConn action conn                       -- runSQL
                      runStdoutLoggingT $ runSqlPool (do runMigration migrateAll) pool -- migration

So, our runSQL function really just calls runQuery and uses the connection it provides to perform some database actions.

Adding People

Now we’re ready to use our database in our application. Let’s change our POST /people function to insert the parsed Person into our database and show an appropriate JSON response. First though, add one one more helper function to generate simple JSON errors:

errorJson :: Int -> Text -> ApiAction ()
errorJson code message =
  json $
    object
    [ "result" .= String "failure"
    , "error" .= object ["code" .= code, "message" .= message]
    ]

Because it’s good practice for an API to always respond with JSON (or your preferred data format), we’ll use this instead of just plain text when reporting errors. Returning error codes is another good practice that allows users to troubleshoot issues more easily.

Now change your post action to use the errorJson function and insert a person into the database:

  post "people" $ do
    maybePerson <- jsonBody :: ApiAction (Maybe Person)
    case maybePerson of
      Nothing -> errorJson 1 "Failed to parse request body as Person"
      Just thePerson -> do
        newId <- runSQL $ insert thePerson
        json $ object ["result" .= String "success", "id" .= newId]

Reload your project and start the server again. Try adding a couple of people:

$ curl -H "Content-Type: application/json" -d '{ "name": "Walter", "age": 50 }' localhost:8080/people
{"result":"success","id":1}
$ curl -H "Content-Type: application/json" -d '{ "name": "Jesse", "age": 22 }' localhost:8080/people
{"result":"success","id":2}

Listing People

Now we’ll change our get action to return a list of all the people in our table:

  get "people" $ do
    allPeople <- runSQL $ selectList [] [Asc PersonId]
    json allPeople

Reload and start the server and go to localhost:8080/people to see your list.

Getting a Specific Person

If you look at the JSON response for localhost:8080/people, you should see that your Person objects now have id keys. These values are automatically inserted by Persistent: we’ll use them to get a person by their id. Add the following route function to your app:

  get ("people" <//> var) $ \personId -> do
    maybePerson <- runSQL $ P.get personId :: ApiAction (Maybe Person)
    case maybePerson of
      Nothing -> errorJson 2 "Could not find a person with matching id"
      Just thePerson -> json thePerson
      

And again, reload and try it out: localhost:8080/people/1.

Finishing Up: Exercises

We’ve implemented the foundation of our API and some of the basic functionality. Practice adding some features yourself by trying out these exercises:

  1. Try adding PUT and DELETE routes yourself. Note that we hid Database.Persist.delete to avoid a naming clash with Web.Spock.delete, so you’ll have to use the P namespace qualifier.
    Click for hints For the PUT action you'll want to use Web.Spock.put and Database.Persist.Class.replace. The Persistent chapter in the Yesod Book has an example of using both replace and delete. For the DELETE action, you may have to explicitly declare your route variable as a PersonId so that GHC knows which type of database object you want to delete. We've explicitly declared values a couple of times in our tutorial, such as decoding to Person and Value and when using Spock actions.


  2. It is good practice to set appropriate HTTP status codes, for example:
    • 201 Created for successful POST and PUT actions
    • 400 Bad Request for failed POST and PUT actions
    • 404 Not Found for GET /people/<id> route

    Try setting appropriate HTTP status codes for your actions

    Click for hints Look at the type of Web.Spock.Action.setStatus: it takes a single argument of type Status and needs to be called in a Spock action context. You'll have to add http-types to your project dependencies and import its Status module to use the Status definitions.


  3. Spock has a error handler for generic errors such missing routes, runtime exceptions, etc. By default, this returns generic HTML pages. Use Web.Spock.Config.spc_errorHandler to change your application to return generic JSON error responses. You can try any other route like localhost:8080/ to see the default 404 error response.
    Click for hints Your current config datatype is being constructed by passing arguments to defaultSpockCfg. You'll want to change this to use the records listed in Web.Spock.Config documentation.
    The spc_errorHandler type signature shows that it takes a Status and returns a Spock action, so you'll need write a function that pattern matches on Status types introduced in the last exercise and returns JSON errors. If you want to use errorJson, you'll have to change its type signature to allow both IO and WebStateM (SpockAction's) monads.