- Overview
- Project Setup
- API Type
- Serving JSON
- Parsing JSON
- Adding a Database
- Running Queries
- Finishing Up: Exercises
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:
- Create a new project using
stack new Spock-rest
- 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:
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:
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 Person
s into
ByteString
s 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:
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
:
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 Person
s. Change
your get
function to the following:
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 Person
s 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:
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:
And these imports below your existing ones:
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:
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:
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:
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:
and your ApiAction
type:
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:
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
:
If you compare the right side of the runSQL
definition to the migration code in the last
section, you’ll see some similarities:
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:
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:
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:
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
:
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:
- Try adding PUT and DELETE routes yourself. Note that we hid
Database.Persist.delete
to avoid a naming clash withWeb.Spock.delete
, so you’ll have to use theP
namespace qualifier.Click for hints
For the PUT action you'll want to useWeb.Spock.put
andDatabase.Persist.Class.replace
. The Persistent chapter in the Yesod Book has an example of using bothreplace
anddelete
. For the DELETE action, you may have to explicitly declare your route variable as aPersonId
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 toPerson
andValue
and when using Spock actions. - It is good practice to set appropriate HTTP status codes, for example:
201 Created
for successful POST and PUT actions400 Bad Request
for failed POST and PUT actions404 Not Found
forGET /people/<id>
route
Try setting appropriate HTTP status codes for your actions
Click for hints
Look at the type ofWeb.Spock.Action.setStatus
: it takes a single argument of typeStatus
and needs to be called in a Spock action context. You'll have to addhttp-types
to your project dependencies and import its Status module to use theStatus
definitions. - 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 todefaultSpockCfg
. You'll want to change this to use the records listed inWeb.Spock.Config
documentation.
Thespc_errorHandler
type signature shows that it takes aStatus
and returns a Spock action, so you'll need write a function that pattern matches onStatus
types introduced in the last exercise and returns JSON errors. If you want to useerrorJson
, you'll have to change its type signature to allow bothIO
andWebStateM
(SpockAction
's) monads.