Last week we looked at the library Scalacache however this week we look at http4s. Written by Alan Devlin he talks about A REST API with http4s and Cats IO Monad.
"http4s is an HTTP library for Scala. http4s has good documentation including a tutorial. However, the tutorial only covers the most basic of applications — and if you are not super comfortable with the approach — it might be difficult to extend it to real-life. At least it was for me! We are going to take it one step further and implement a very simple CRUD (Create Read Update Delete) web-server application. Hopefully this should give you enough to get started with a real API.
All the code for this demo can be found on github.
Why http4s?
The App.
We are going to keep things simple. The object we are are going to be using is a Hut:
case class Hut(name: String)
Our hut class has one field: name. We are also going to have a HutWithId:
case class HutWithId(id: String, name: String)
For simplicity we are going to store our hut objects in memory, rather than use a database.
Setup the App.
We are going to use the template from http4s to bootstrap our app. Run:
$ sbt -sbt-version 1.1.1 new http4s/http4s.g8
… and answer the prompts — see http://http4s.org/v0.18/ for further guidance.
All going well you should be able to run your app and make a simple request like:
$ curl -i http://localhost:8080/hello/world
Build the App. Storage layer.
We are going to add CRUD functionality: The HutRepository is going to store and retrieve huts:
package io.github.spf3000.hutsapi
import java.util.UUID
import scala.collection.mutable.ListBuffer
import cats.effect.IO
import io.github.spf3000.hutsapi.entities._
final class HutRepository(private val huts: ListBuffer[HutWithId]) {
val makeId: IO[String] = IO { UUID.randomUUID().toString }
def getHut(id: String): IO[Option[HutWithId]] =
IO { huts.find(_.id == id) }
def addHut(hut: Hut): IO[String] =
for {
uuid <- makeId
_ <- IO { huts += hutWithId(hut, uuid) }
} yield uuid
def updateHut(hutWithId: HutWithId): IO[Unit] = {
for {
_ <- IO { huts -= hutWithId }
_ <- IO { huts += hutWithId }
} yield()
}
def deleteHut(hutId: String): IO[Unit] =
for {
h <- IO {huts.find(_.id == hutId).map{
h => IO {huts -= h }
}
}
} yield ()
def hutWithId(hut: Hut, id: String): HutWithId =
HutWithId(id, hut.name)
}
object HutRepository {
def empty: IO[HutRepository] = IO{new HutRepository(ListBuffer())}
}
Ok so you might notice that if you forget about all the IO’s it looks very impure — I am mutating state in place, (with the += and -=) and makeId generates a random number — also not referentially transparent. But all these functions are pure! In fact things are so pure, I don’t even have to worry about whether Scala is going to store things (memoizations), i.e. observe:
val makeId: IO[String] = IO { UUID.randomUUID().toString }
this could be a def — it actually doesn’t matter — with IO you can just use defs when you need arguments and vals when you don’t, as you have control over when things are evaluated.
Notice also that the creation of the ListBuffer is in IO as this also breaks referential transparency.
Build the App. Http layer.
In the http layer we are going to call our repository methods and return some of the pre-defined responses that http4s gives us such as OK and NotFound
package io.github.spf3000.hutsapi
import cats.effect.IO
import fs2.StreamApp
import io.circe.generic.auto._
import org.http4s._
import org.http4s.circe._
import org.http4s.dsl.Http4sDsl
import org.http4s.server.blaze.BlazeBuilder
import scala.concurrent.ExecutionContext.Implicits.global
import entities.Hut
import entities._
object HutServer extends StreamApp[IO] with Http4sDsl[IO] {
implicit val decoder = jsonOf[IO, Hut]
implicit val decoder1 = jsonOf[IO, HutWithId]
implicit val encoder = jsonEncoderOf[IO, HutWithId]
val hutRepo = HutRepository.empty.unsafeRunSync()
val HUTS = "huts"
val service = HttpService[IO] {
case GET -> Root / HUTS / hutId =>
hutRepo.getHut(hutId)
.flatMap(_.fold(NotFound())(Ok(_)))
case req @ POST -> Root / HUTS =>
req.as[Hut].flatMap(hutRepo.addHut).flatMap(Created(_))
case req @ PUT -> Root / HUTS =>
req.as[HutWithId]
.flatMap(hutRepo.updateHut)
.flatMap(Ok(_))
case DELETE -> Root / HUTS / hutId =>
hutRepo.deleteHut(hutId)
.flatMap(_ => NoContent())
}
def stream(args: List[String], requestShutdown: IO[Unit]) =
BlazeBuilder[IO]
.bindHttp(8080, "0.0.0.0")
.mountService(service, "/")
.serve
}
Use the App.
Open a terminal window, cd into the top directory of the app and type:
sbt run
Now you are up and running — and with a clear conscience! To create a new hut you can open another terminal and run the following command:
curl -v -H "Content-Type: application/json" -X POST http://localhost:8080/huts -d '{"name":"River Hut"}'
(this should give you back the id of your newly created hut).
You can update a hut*:
curl -v -H "Content-Type: application/json" -X PUT http://localhost:8080/huts -d '{"id":"123","name":"Mountain Hut"}'
* Not strictly an update — in this instance we are creating a hut with a specified id. In the author’s humble opinion PUT is not exactly equivalent to update, it’s an idempotent write.
GET a hut:
$ curl -i http://localhost:8080/huts/123
Which will give a 200 (OK) response and the json of our hut:
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 20 Mar 2018 13:57:59 GMT
Content-Length: 34
{"id":"123","name":"Mountain Hut"}
and finally delete a hut:
curl -v -X DELETE http://localhost:8080/huts/123
And there you have it — You have basically done nothing but IO and yet the code is totally pure! It’s amazing."
Article originally posted on Medium.com and written by Alan Devlin.