http4s example of REST API

So you decided to try http4s to create some web app or an API. Naturally your first step is to read official docs. Then you may notice that it’s not helpful at some point (there you see just some syntax and general approaches to deal with common http stuff). Finally you start google for some http4s examples, preferably with CRUD, because it’s almost a standard way to start acquaintance with new http library.

Before we start, I want to emphasis a main goal of this article — demonstrate how to develop a simple CRUD http4s application. I’m not going to focus on a database interaction, so we will use in-memory data structure (Map).

Imminent steps

skip this section if you already have an http4s skeleton in the IDE

I’m confident that you already have some basic skills and knowledge about Scala. Literally it means you have installed IDE, SBT and know some basic programming skills with Scala. Also I’m going to use some trivial functional programming technics, such as data modeling, but I believe it will not be something difficult, just because it’s pretty logical.

Now we are ready! Open a command line tool — terminal on Mac, console on Windows (or what is used on Windows now?) and execute following command:

sbt -sbt-version 1.2.8 new http4s/http4s.g8 -b 0.20

It’s easy to notice that I’m writing this article at a time when the latest version of SBT is 1.2.8 and 0.20 is a stable version of http4s. By executing this command you start initialization of a new http4s project from its giter8 template.

Next follow the instructions, set a project name, sbt version, scala version etc. You need to end up with a successfully generated http4s “blank” project. Then import it to your IDE (IDEA, Eclipse or whatever). Now you are ready for development.

Application domain

There are many good traditions in a software development. For example to start learning a new programming language from a “Hello world!” program. Unfortunately I don’t know any traditions related to learning a new http library or framework, so I suggest to stick with a books domain model. That means that we are going to build an http API for following operations:

  1. add a book
  2. get a book
  3. update a book
  4. delete a book
  5. list all books

Now when we know what we are going to build, let’s describe a model!

import scala.util.Random

object BookModels {
  type Title = String
  type Author = String
  type Id = String
  final case class BookId(value: String = Random.alphanumeric.take(8).foldLeft("")((result, c) => result + c))

  type Message = String
}
case class Book(title: Title, author: Author)
case class BookWithId(id: Id, title: Title, author: Author)

In the code snippet above I did several things. Firstly I create custom type aliases to standard Scala String type. Secondly I create BookId class, to handle id uniqueness. Finally I composed Book and BookWithId models.

In general you can use strict String type without creating noisy aliases. Also there is a chance to avoid creation of two case classes for a book representation with and without id (with help of Option type for id). But I used to create such a data models, before implementing any domain logic.

Data layer

As I already mentioned at the beginning of this post, I’m not going to dive into databases, so a data layer will be a simple set of operations on top of mutable HashMap.

import cats.effect.IO
import cats.implicits._

trait BookRepo {

  def addBook(book: Book): IO[BookId]
  def getBook(id: BookId): IO[Option[BookWithId]]
  def deleteBook(id: BookId): IO[Either[Message, Unit]]
  def updateBook(id: BookId, book: Book): IO[Either[Message, Unit]]
  def getBooks(): IO[List[BookWithId]]

}

An important thing to notice about the BookRepo — returning types are wrapped in IO. So this is a good time to refresh in memory what does IO serves for. Here is an implementation of the repository:

object BookRepo {

  class DummyImpl extends BookRepo {

    import scala.collection.mutable.HashMap
    val storage = HashMap[BookId, Book]().empty

    override def addBook(book: Book): IO[BookId] = IO {
      val bookId = BookId()
      storage.put(bookId, book)
      bookId
    }

    override def getBook(id: BookId): IO[Option[BookWithId]] = IO {
      storage.get(id).map(book => BookWithId(id.value, book.title, book.author))
    }

    override def deleteBook(id: BookId): IO[Either[Message, Unit]] =
      for {
        removedBook <- IO(storage.remove(id))
        result = removedBook.toRight(s"Book with ${id.value} not found").void
      } yield result

    override def updateBook(id: BookId, book: Book): IO[Either[Message, Unit]] = {
      for {
        bookOpt <- getBook(id)
        _ <- IO(bookOpt.toRight(s"Book with ${id.value} not found").void)
        updatedBook = storage.put(id, book)
                        .toRight(s"Book with ${id.value} not found").void
      } yield updatedBook
    }

    override def getBooks(): IO[List[BookWithId]] = IO {
      storage.map {case (id, book) => BookWithId(id.value, book.title, book.author)}.toList
    }
  }

}

I don’t know what to say about DummyImpl, so I’d better answer any question regarding this code in the comments section in the end of this article.

HTTP layer

When you have defined a data model and a data access layer for it, it’s a good time to make a next move. In our case we need to create an HTTP interface to interact with books. Hence on this stage we need to provide an HTTP endpoint for each of five functions from BookRepo:

  1. add a book — POST /books
  2. get a book — GET /books/{id}
  3. update a book — PUT /books/{id}
  4. delete a book — DELETE /books/{id}
  5. list all books — GET /books

That’s how these endpoints look in terms of http4s syntax:

import cats.effect.IO
import io.circe.Json
import org.http4s.HttpRoutes
import io.circe.generic.auto._
import org.http4s.circe.CirceEntityCodec._
import org.http4s.dsl.Http4sDsl

object BookRoutes {

  private def errorBody(message: Message) = Json.obj(
    ("message", Json.fromString(message))
  )

  def routes(bookRepo: BookRepo): HttpRoutes[IO] = {

    val dsl = new Http4sDsl[IO]{}
    import dsl._

    HttpRoutes.of[IO] {
      case _ @ GET -> Root / "books" =>
        bookRepo.getBooks().flatMap(books => Ok(books))

      case req @ POST -> Root / "books" =>
        req.decode[Book] { book =>
          bookRepo.addBook(book) flatMap(id =>
            Created(Json.obj(("id", Json.fromString(id.value))))
          )
        }

      case _ @ GET -> Root / "books" / id =>
        bookRepo.getBook(BookId(id)) flatMap {
          case None => NotFound()
          case Some(book) => Ok(book)
        }

      case req @ PUT -> Root / "books" / id =>
        req.decode[Book] { book =>
          bookRepo.updateBook(BookId(id), book) flatMap {
            case Left(message) => NotFound(errorBody(message))
            case Right(_) => Ok()
          }
        }

      case _ @ DELETE -> Root / "books" / id =>
        bookRepo.deleteBook(BookId(id)) flatMap {
          case Left(message) => NotFound(errorBody(message))
          case Right(_) => Ok()
        }
    }
  }

}

Notice that import org.http4s.circe.CirceEntityCodec._ is used for automatic serialization / deserialization of models (case classes).

The app launch

One step left to launch a working application. In http4s routes do not handle any requests, until you explicitly associate them with running server. So here is what we need to do in order to make books REST API live:

import cats.effect.{ExitCode, IO, IOApp}
import org.http4s.server.Router
import org.http4s.implicits._
import org.http4s.server.blaze._
import cats.implicits._


object Main extends IOApp {

  private val bookRepo: BookRepo = new DummyImpl

  val httpRoutes = Router[IO](
    "/" -> BookRoutes.routes(bookRepo)
  ).orNotFound

  override def run(args: List[String]): IO[ExitCode] = {

    BlazeServerBuilder[IO]
      .bindHttp(9000, "0.0.0.0")
      .withHttpApp(httpRoutes)
      .serve
      .compile
      .drain
      .as(ExitCode.Success)
  }

}

In order to test how the application handles http request, we need to run the Main object. Now we can execute several test requests:

>curl -X POST \
>   localhost:9000/books \
>   -H 'Content-Type: application/json' \
>   -d '{
> "title": "The Sea Wolf",
> "author": "Jack London"
> }'\
> 
>{"id":"EqD66fJG"}
>
>curl -X GET localhost:9000/books \
> 
>[{"id":"EqD66fJG","title":"The Sea Wolf","author":"Jack London"}]

That’s how you can use http4s in order to construct REST APIs. I believe that this blog post was helpful for you, even if it’s just another article about building a sample CRUD application with http4s 🙂

About The Author

Mathematician, programmer, wrestler, last action hero... Java / Scala architect, trainer, entrepreneur, author of this blog

Close