Http4s, Doobie and Circe: The Functional Web Stack
This document presents a comprehensive guide for building a CRUD web application using the functional web stack http4s, doobie, and circe in Scala. It covers data conversions between JSON and JDBC formats, automatic derivation of JSON codecs with Circe, as well as mapping and querying a database using Doobie. The document also explains how to set up an HTTP service with http4s and includes examples for handling requests and responses, integrating HTML templating, and streaming data.
Http4s, Doobie and Circe: The Functional Web Stack
1.
THE FUNCTIONAL WEBSTACK
HTTP4S, DOOBIE & CIRCE
Gary Coady
gcoady@gilt.com
2.
LET’S BUILD AWEBSITE
• It’s going to be CRUD (create/read/update/delete)
• How do we convert data to/from database structure?
• How do we convert data to/from JSON?
• Converting from one format to another is most of a developer’s job!
3.
CONVERTING TO/FROM JSON
String
Instant
Int
caseclass MyClass(
s: String,
t: Instant,
i: Int)
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
Case Class JSON Object
4.
CONVERTING TO/FROM JDBC
String
Instant
Int
caseclass MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
5.
JDBC AND JSONCOMBINED
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
JSON Object
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
6.
JSON CONVERSION WITHCIRCE
• Very fast, usable JSON library
• Automatic derivation of JSON codecs for case classes
• Integration available with http4s
• http://circe.io
CONVERTING CASE CLASSESWITH CIRCE
Automatic
import io.circe.generic.auto._
Manual
// for case class Person(id: Int, name: String)
object Person {
implicit val decodePerson: Decoder[Person] =
Decoder.forProduct2("id", "name")(Person.apply)
implicit val encodePerson: Encoder[Person] =
Encoder.forProduct2("id", "name")(p =>
(p.id, p.name)
)
}
10.
CONVERTING TO/FROM JSON
String
Instant
Int
caseclass MyClass(
s: String,
t: Instant,
i: Int)
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
Case Class JSON Object
11.
JDBC USING DOOBIE
•Doobie is a “pure functional JDBC layer for Scala”
• Complete representation of Java JDBC API
• https://github.com/tpolecat/doobie
• tpolecat.github.io/doobie-0.2.3/00-index.html (Documentation)
12.
JDBC MAPPING WITHDOOBIE
sealed trait Meta[A] {
/** Destination JDBC types to which values of type `A` can be written. */
def jdbcTarget: NonEmptyList[JdbcType]
/** Source JDBC types from which values of type `A` can be read. */
def jdbcSource: NonEmptyList[JdbcType]
/** Constructor for a `getXXX` operation for type `A` at a given index. */
val get: Int => RS.ResultSetIO[A]
/** Constructor for a `setXXX` operation for a given `A` at a given index. */
val set: (Int, A) => PS.PreparedStatementIO[Unit]
/** Constructor for an `updateXXX` operation for a given `A` at a given index. */
val update: (Int, A) => RS.ResultSetIO[Unit]
/** Constructor for a `setNull` operation for the primary JDBC type, at a given index. */
val setNull: Int => PS.PreparedStatementIO[Unit]
}
13.
JDBC MAPPING WITHDOOBIE
implicit val metaInstant =
Meta[java.sql.Timestamp].nxmap(
_.toInstant,
(i: Instant) => new java.sql.Timestamp(i.toEpochMilli)
)
Reusing an existing Meta definition
Meta definitions exist for:
Byte
Short
Int
Boolean
String
Array[Byte]
BigDecimal
Long
Float
Double
java.math.BigDecimal
java.sql.Time
java.sql.TimeStamp
java.sql.Date
java.util.Date
QUERIES WITH DOOBIE
sql"selectid, name from people".query[Person]
Table "public.people"
Column | Type | Modifiers
---------------+--------------------------+-----------------------------------------------------
id | integer | not null default nextval('people_id_seq'::regclass)
name | text |
Indexes:
"people_pkey" PRIMARY KEY, btree (id)
Given the table
Define a query
sql"select id, name from people where id = $id".query[Person]
Using prepared statements & variable interpolation
16.
DEFINE THE EXPECTEDRESULT SIZE
• myQuery.unique — Expect a single row from the query
• myQuery.option — Expect 0-1 rows from the query, return an Option
• myQuery.list — Return the results as a List
• myQuery.vector — Return the results as a Vector
• myQuery.process — Return the results as a stream of data
17.
RUNNING QUERIES WITHDOOBIE
Define a Transactor for your Database
val xa = DriverManagerTransactor[Task](
"org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""
)
Run your query using the Transactor
myQuery.list.transact(xa)
Your query runs in a transaction (rollback on uncaught exception)
18.
UPDATES WITH DOOBIE
Call.update instead of .query (returns number of rows modified)
def updatePerson(id: Int, name: String): ConnectionIO[Int] =
sql"update people set name=$name where id=$id"
.update
def updatePerson(id: Int, name: String): ConnectionIO[Person] =
sql"update people set name=$name where id=$id"
.update
.withUniqueGeneratedKeys("id", "name")
.withUniqueGeneratedKeys provides data from updated row
.withGeneratedKeys provides a stream of data, when multiple rows are updated
19.
CONVERTING TO/FROM JDBC
String
Instant
Int
caseclass MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
20.
JDBC AND JSONCOMBINED
String
Instant
Int
case class MyClass(
s: String,
t: Instant,
i: Int)
[0]@text
[1]@timestamptz
[2]@bigint
JDBC ResultSet(
0: text,
1: timestamptz,
2: bigint)
Case Class ResultSet
“s”@JSON String
“t”@JSON Number
“i”@JSON Number
JSON Object
Json.obj(
“s”: Json.str,
“t”: Json.number,
“i”: Json.number)
21.
HTTP4S
• http4s isa typeful, purely functional HTTP library for client and server
applications written in Scala
• http4s.org/
• https://github.com/http4s/http4s
• https://github.com/TechEmpower/FrameworkBenchmarks/tree/master/
frameworks/Scala/http4s (from TechEmpower benchmarks)
• www.lyranthe.org/http4s/ (some programming guides)
22.
WRITING A WEBSERVICE WITH HTTP4S
type HttpService = Service[Request, Response]
represents
Request => Task[Response]
Three requirements:
• Setup service
• Parse request
• Generate response
23.
PATTERN MATCHING ONTHE REQUEST
<METHOD> -> <PATH>
Extract Method and Path from Request
For example
case GET -> path
Root / "people" / id
Extract path components from Path
PARSING REQUEST BODY
RegisterCirce as JSON decoder
implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) =
org.http4s.circe.jsonOf[A]
req.decode[Person] { person =>
...
}
Decode to a Person object
26.
CREATING RESPONSE
Ok(Person(1, "Name"))
Outputresponse code and any type with an EntityEncoder
Ok("Hello world")
Output response code and String as body
implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) =
org.http4s.circe.jsonEncoderOf[A]
27.
PUTTING IT ALLTOGETHER
case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)
case class PersonForm(firstName: String, familyName: String)
object PersonDAO {
implicit val metaInstant =
Meta[java.sql.Timestamp].nxmap(
_.toInstant,
(i: Instant) => new java.sql.Timestamp(i.toEpochMilli)
)
val listPeople: ConnectionIO[List[Person]] =
sql"select id, first_name, family_name, registered_at from people"
.query[Person]
.list
def getPerson(id: Long): ConnectionIO[Option[Person]] =
sql"select id, first_name, family_name, registered_at from people where id = $id"
.query[Person]
.option
def updatePerson(id: Int, firstName: String, familyName: String): ConnectionIO[Person] =
sql"update people set first_name=$firstName, family_name=$familyName where id=$id"
.update
.withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")
def insertPerson(firstName: String, familyName: String, registeredAt: Instant = Instant.now()): ConnectionIO[Person] =
sql"insert into people (first_name, family_name, registered_at) values ($firstName, $familyName, $registeredAt)"
.update
.withUniqueGeneratedKeys("id", "first_name", "family_name", "registered_at")
}
28.
PUTTING IT ALLTOGETHER
case class Person(id: Int, firstName: String, familyName: String, registeredAt: Instant)
case class PersonForm(firstName: String, familyName: String)
object DemoService {
implicit def circeJsonDecoder[A](implicit decoder: Decoder[A]) =
org.http4s.circe.jsonOf[A]
implicit def circeJsonEncoder[A](implicit encoder: Encoder[A]) =
org.http4s.circe.jsonEncoderOf[A]
def service(xa: Transactor[Task]) = HttpService {
case GET -> Root / "people" =>
Ok(PersonDAO.listPeople.transact(xa))
case GET -> Root / "people" / IntVar(id) =>
for {
person <- PersonDAO.getPerson(id).transact(xa)
result <- person.fold(NotFound())(Ok.apply)
} yield result
case req @ PUT -> Root / "people" / IntVar(id) =>
req.decode[PersonForm] { form =>
Ok(PersonDAO.updatePerson(id, form.firstName, form.familyName).transact(xa))
}
case req @ POST -> Root / "people" =>
req.decode[PersonForm] { form =>
Ok(PersonDAO.insertPerson(form.firstName, form.familyName).transact(xa))
}
}
}
29.
PUTTING IT ALLTOGETHER
object Main {
def main(args: Array[String]): Unit = {
val xa = DriverManagerTransactor[Task](
"org.postgresql.Driver", "jdbc:postgresql:demo", "demo", ""
)
val server =
BlazeBuilder
.bindHttp(8080)
.mountService(DemoService.service(xa))
.run
server.awaitShutdown()
}
}
30.
ADDING HTML TEMPLATINGWITH TWIRL
Add to project/plugins.sbt
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1")
Place templates in src/main/twirl
STREAMING DATA WITHSCALAZ-STREAM
• Response can take a scalaz-stream Process
• Doobie can create a scalaz-stream Process from a Resultset
• Data sent incrementally using chunked Transfer-Encoding
val streamPeople: Process[ConnectionIO, Person] =
sql"select id, first_name, family_name, registered_at from people"
.query[Person]
.process
case GET -> Root / "stream" =>
Ok(PersonDAO.streamPeople.transact(xa).map(p => p.id + "n"))