1. Introduction
In this tutorial, we will be talking about building REST APIs using Finch.
2. Finch and Finagle
What is Finch? According to its GitHub readme:
Finch is a thin layer of purely functional basic blocks atop of Finagle for building composable HTTP APIs. Its mission is to provide the developers simple and robust HTTP primitives being as close as possible to the bare metal Finagle API.
In other words, it is a purely functional *frontend* for Finagle, a powerful RPC system for building high-concurrency servers on the JVM. What is good about Finagle is, various network servers and clients (services) can be programmed using common building blocks, no matter the protocol a particular service uses. Finch then, is mainly focused on building HTTP services. An HTTP service is simply a pair of a request and a response: for a given, specific kind of request, we send back an appropriate response.
Both Finch and Finagle are developed and maintained by Twitter.
3. Dependencies
To use Finch, we need to add the finch-core and finch-circe dependencies:
libraryDependencies ++= Seq(
"com.github.finagle" %% "finch-core" % "0.31.0",
"com.github.finagle" %% "finch-circe" % "0.31.0",
"io.circe" %% "circe-generic" % "0.9.0"
)
4. Hello, World API
Let’s start with a simple Hello, World! example:
object Main extends App {
val hello: Endpoint[String] = get("hello") { Ok("Hello, World!") }
Await.ready(Http.server.serve(":8081", hello.toService))
}
In line 2, we defined a new Finch endpoint. A Finch endpoint is simply an abstraction: an HTTP endpoint associated with an HTTP method, accepting a request, and responding with a particular response we have defined. As we will see later, various endpoints can be composed very easily, allowing us to build rich APIs.
Now, recall that Finch is a pure functional wrapper for Finagle; it means we are still dealing with Finagle under the hood. Because the Http server itself comes from Finagle and knows how to serve Finagle services only, we have to convert our Finch endpoint to a Finagle service. This is precisely what calling the toService method on line 4 does.
Lastly, we pass it as an argument to the serve method for our Http server. The serve method knows how to serve our service over Http.
If we execute sbt run, we can navigate to http://localhost:8081/hello to see the result:
$ curl localhost:8081/hello
Hello, World!
4.1. Accepting a JSON Body
Let’s add another endpoint. This time, we will accept a JSON body containing first and last name using a composite Finch Endpoint. To do so, we need to match against a path, and a request body:
case class FullName(first: String, last: String)
val helloName: Endpoint[String] = post("hello" :: jsonBody[FullName]) { name: FullName =>
Ok(s"Hello, ${name.first} ${name.last}!")
}
Lastly, we modify the service argument to the serve method call of the Http server like this:
Await.ready(Http.server.serve(":8081", (hello :+: helloName).toService))
Let’s call the API with some JSON body:
curl -X POST -H "Content-Type: application/json" -d '{"first":"John", "last":"Doe"}' localhost:8081/hello
"Hello, John Doe!"
As we mentioned previously, Finch endpoints can be composed. To see how we can do that and what composition means, let us recall the symbols we used in the above example, namely (::) and (:+:). They are called combinators.
The first one, (::), we read as “and then”. In the example, it means that our endpoint is composed of the string “hello” and a FullName JSON body. In other words, it describes our new HTTP endpoint. It should match an endpoint whose path is “/hello” and accepts a JSON body containing two fields: first and last.
We read (:+:) as “or else” and is used to describe alternatives: it matches either endpoint hello or else, helloName.
4.2. A Note About JSON Serialization and Parsing
There are many Scala libraries for handling JSON data. In this tutorial, we are using Circe, a purely functional library built on top of Cats and Shapeless.
What is most notable is that it has the capability of automagically mapping JSON to and from plain Scala case classes without any special effort on our side.
Finch provides out of the box integration not only for Circe, but also for other libraries, such as Argonaut, Jackson, JSON4s, and so on.
5. A Complete REST API Example
We will now attempt to build a more complete API based on REST principles for a simple todo app. To store our todo data, we will use an SQLite database. For executing our queries, we will use Doobie, a purely functional, strongly typed JDBC layer for Scala.
Instead of the standard Endpoint we’ve been using so far, we want to handle our requests in a reactive fashion using Cats IO effects. For this purpose, we want an Endpoint[IO, _]. This will allow us to interleave and make database calls in a safe sequential manner.
5.1. Dependencies
The dependencies we need this time will be slightly different. Instead of finch-core and finch, *we will switch to the finchx-* counterparts, which will allow us to use a polymorphic Endpoint, such as Endpoint[IO, _].*
In addition, we will include some helper dependencies for dealing with IO and dependencies for dealing with databases:
libraryDependencies ++= Seq(
"com.github.finagle" %% "finchx-core" % "0.31.0",
"com.github.finagle" %% "finchx-circe" % "0.31.0",
"io.circe" %% "circe-generic" % "0.9.0",
"org.typelevel" %% "cats-effect" % "2.1.3",
"org.typelevel" %% "cats-core" % "2.1.1",
"org.xerial" % "sqlite-jdbc" % "3.31.1",
"org.tpolecat" %% "doobie-core" % "0.8.8",
)
5.2. Create a New Todo
Let’s create our Todo model. We will use a case class as Circe will be able to figure out JSON conversions for us automatically:
case class Todo(
id: Option[Int],
name: String,
description: String,
done: Boolean
)
And then we define the corresponding endpoint:
val createTodo: Endpoint[IO, Todo] = post(todosPath :: jsonBody[Todo]) { todo: Todo =>
for {
id <- sql"insert into todo (name, description, done) values (${todo.name}, ${todo.description}, ${todo.done})"
.update
.withUniqueGeneratedKeys[Int]("id")
.transact(xa)
created <- sql"select * from todo where id = $id"
.query[Todo]
.unique
.transact(xa)
} yield Created(created)
}
As we can see, our endpoint consists of a post request accepting a JSON body conforming to our Todo model.
We take what is in the body, create a new record in our database and return it in the response.
Indeed, if we POST something to our new endpoint, we can verify that it will give us back the newly created record:
$ curl -X POST -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": false}' \
localhost:8081/todos
{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}
5.3. Get a Specific Todo
Retrieving an object from the database is also fairly straightforward:
val getTodo: Endpoint[IO, Todo] = get(todosPath :: path[Int]) { id: Int =>
for {
todos <- sql"select * from todo where id = $id"
.query[Todo]
.to[Set]
.transact(xa)
} yield todos.headOption match {
case None => NotFound(new Exception("Record not found"))
case Some(todo) => Ok(todo)
}
}
This is an endpoint for a get request, accepting an integer parameter – an id, returning a Todo record in the response.
Since it is possible the todo we are looking for is not in our database, we are matching against an Option value and responding according to the result of our query:
$ curl localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":false}
Asking for a nonexisting todo will return a 404:
$ curl -i localhost:8081/todos/0
HTTP/1.1 404 Not Found
Date: Wed, 27 May 2020 00:33:28 GMT
Server: Finch
Content-Length: 0
5.4. Get All Todos
Similarly, we will let our API users fetch a complete list of todos:
val getTodos: Endpoint[IO, Seq[Todo]] = get(todosPath) {
for {
todos <- sql"select * from todo"
.query[Todo]
.to[Seq]
.transact(xa)
} yield Ok(todos)
}
This time, we don’t accept any parameters since we want to match against all records:
$ curl localhost:8081/todos
[{"id":1,"name":"Hello, world","description":"From Baeldung","done":false},
{"id":2,"name":"Update Endpoint","description":"To be able to mark as completed","done":false},
{"id":3,"name":"Delete Todo Endpoint","description":"To be able to delete todos","done":false}]
5.5. Mark as Done
We would also like to be able to make updates to our todos. Let’s say we want to mark a todo as done or make changes to the name or description. Let’s add the following endpoint for this purpose:
val updateTodo: Endpoint[IO, Todo] = put(todosPath :: path[Int] :: jsonBody[Todo]) { (id: Int, todo: Todo) =>
for {
_ <- sql"update todo set name = ${todo.name}, description = ${todo.description}, done = ${todo.done} where id = $id"
.update
.run
.transact(xa)
todo <- sql"select * from todo where id = $id"
.query[Todo]
.unique
.transact(xa)
} yield Ok(todo)
}
This particular endpoint is similar to the create endpoint from before. The difference is in the method: it is put instead of post, carrying the meaning of updating some existing data.
We want to update one of our todo entries and mark it as done:
$ curl -X PUT -H "Content-Type: application/json" -d ' \
{"name": "Hello, world", \
"description": "From Baeldung", \
"done": true}' \
localhost:8081/todos/1
{"id":1,"name":"Hello, world","description":"From Baeldung","done":true}
5.6. Delete a Todo
Finally, let’s make it possible to delete a todo as well:
val deleteTodo: Endpoint[IO, Unit] = delete(todosPath :: path[Int]) { id: Int =>
for {
_ <- sql"delete from todo where id = $id"
.update
.run
.transact(xa)
} yield NoContent
}
Again, notice a parallel here. This endpoint is very similar to our get endpoint since it accepts an id parameter. The difference is that we use delete instead of get.
There is no content in the response when we delete a record:
$ curl -i -X DELETE localhost:8081/todos/3
HTTP/1.1 204 No Content
Date: Wed, 27 May 2020 01:05:08 GMT
Server: Finch
6. Conclusion
In the previous sections, we took a look at building REST APIs with Twitter’s Finch using functional programming principles.
For those who desire a more detailed setup, check out the official documentation for Finch.
We have also shown how easy it is to integrate it with Doobie for executing SQL statements.
As usual, all code examples are available over on GitHub.