Building a REST API in Scala 3 using Iron and Cats
Tags: scala, cats, tutorial
Published: 2021-07-27 14:59:38

# Overview
Cats is a good ecosystem to create concurrent REST APIs, thanks to various cats-based libraries: http4s, circe, cats-effect etc...

In this tutorial, we're going to show the synergy between these libraries and Iron, a type constraint system for Scala.

# Simple Cats/Http4s API
We're going to build a minimal example of a REST API using the Cats ecosystem and Iron.

This API will provide a pseudo register system via `/register` asking for a JSON body
```json
{
  "_comment": "Example request body",
  "username":"Iltotore",
  "email":"me@myemail.com",
  "password":"Abc123"
}
```

and returning the newly created Account as JSON.

## Setup the project
Firstly, we need to setup our dependencies. In this tutorial, I will use the [Mill build tool](https://com-lihaoyi.github.io/mill/mill/Intro_to_Mill.html).

We start with a simple Scala module in our build.sc:
```scala
import mill._, scalalib._

object main extends ScalaModule {
  
  def scalaVersion = "3.0.0"
}
```

In this tutorial we will use the following libraries:
- [Http4s](https://http4s.org/) with Blaze server
- [Circe (JSON)](https://circe.github.io/circe/)
- [Iron (Type constraints)](https://github.com/Iltotore/iron/)

Our build.sc should now look like this:
```scala
import mill._, scalalib._

object main extends ScalaModule {
  
  def scalaVersion = "3.0.0"

  def http4sVersion = "0.23.0-RC1"

  //Http4s dependencies
  ivy"org.http4s::http4s-core:$http4sVersion",
  ivy"org.http4s::http4s-dsl:$http4sVersion",
  ivy"org.http4s::http4s-blaze-server:$http4sVersion",
  ivy"org.http4s::http4s-circe:$http4sVersion",

  //Circe dependencies
  ivy"io.circe::circe-core:0.14.1",
  ivy"io.circe::circe-generic:0.14.1",

  //Iron with String, Cats and Circe modules
  ivy"io.github.iltotore::iron:1.1",
  ivy"io.github.iltotore::iron-string:1.1-0.1.0",
  ivy"io.github.iltotore::iron-cats:1.1-0.1.0",
  ivy"io.github.iltotore::iron-circe:1.1-0.1.0"
}
```

## Setup the http4s server
Before focusing on our Account system, we need to setup an http server first. Http4s and Cats offer a simple way to do this.

Firstly create a service:
```scala
//Basic http4s imports
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._

object HttpServer {

  val service = HttpRoutes.of[IO] {

    case _ => Ok("Hello World")
  }
}
```
In the `HttpRoutes.of` block, we can pattern match on the input request (http method, route...). We will come back to our service later.

Now, we need to create a Blaze server. We will use the example of [Http4s service's documentation](https://http4s.org/v0.21/service/):
```scala
import cats.effect._
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import scala.concurrent.ExecutionContext

//An IOApp will handle shutdown gracefully for us when receiving the SIGTERM signal
object Main extends IOApp {

  override def run(args: List[String]): IO[ExitCode] = BlazeServerBuilder[IO](ExecutionContext.global)
    .bindHttp(8080, "localhost")
    .withHttpApp(HttpServer.service.orNotFound)
    .serve
    .compile //Allow conversion to IO
    .drain //Remove output
    .as(ExitCode.Success) //Set the output as Success

}
```

We can now run our `Main`. But our service in `HttpServer` is currently not that useful: it only returns Ok - "Hello World".

## Account creation
Before modifying our service, we're going to create an Account case class:
```scala
case class Account(username: String, email: String, password: String)
```

Now, we need to check inputs when creating a new Account:
- Is the username alphanumeric ?
- Is the email a valid email scheme ?
- Does the password contain at least an upper, a lower and a number ?

Let's create these validators using Cats' [Validated](https://typelevel.org/cats/datatypes/validated.html):
```scala
import cats.data._, cats.implicits._, cats.syntax.apply._

case class Account(username: String, email: String, password: String)

object Account {

  def validateUsername(username: String): ValidatedNec[String, String] =
    Validated.condNec(username.matches("^[a-zA-Z0-9]+"), username, s"$username should be alphanumeric")

  def validateEmail(email: String): ValidatedNec[String, String] =
    Validated.condNec(email.matches("^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$"), email, s"$email should be a valid email")

  def validatePassword(password: String): ValidatedNec[String, String] =
    Validated.condNec(password.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]+$"), password, s"$password should contain at least an upper, a lower and a number")

  def createAccount(username: String, email: String, password: String): ValidatedNec[String, Account] = (
    validateUsername(username),
    validateEmail(email),
    validatePassword(password)
  ).parMapN(Account.apply)
}
```

We can now create an account using `Account.createAccount`:
```scala
//Valid(Account(...))
Account.createAccount("Iltotore", "thisismymail@gmail.com", "SafePassword1")

//Invalid(NonEmptyChain("Value should be alphanumeric")))
Account.createAccount("Il_totore", "thisismymail@gmail.com", "SafePassword1")

/* 
 * Invalid(NonEmptyChain(
 *   "Value should be alphanumeric"),
 *   "Value must contain at least an upper, a lower and a number")
 * ))
 */
Account.createAccount("Il_totore", "thisismymail@gmail.com", "this_is_not_fine")
```

## Serialization
Before finishing our service, we need to deserialize the request and serialize the response. Fortunately, circe provides Decoders/Encoders for String and Cats' Validated. However, we need to create a Decoder and an Encoder for our Account. Circe offers a simple way to do it:
```scala
import cats.implicits._, cats.syntax.apply._

import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto._

case class Account(username: String, email: String, password: String)

object Account {

  //... (see code above)

  //Get the fields `username`, `email` and `password` and pass them into Account.createAccount
  inline given Decoder[ValidatedNec[String, Account]] =
    Decoder.forProduct3("username", "email", "password")(createAccount)

  //Automatically creates an Encoder from the case class Account
  inline given Encoder[Account] = deriveEncoder
}
```

Now, we can use these codecs in our service.


## Finishing our service
In this example, we will just return the created Account as JSON. Here is the full code:
```scala
//Circe imports
import io.circe.syntax._, io.circe.disjunctionCodecs._, io.circe.Encoder._, io.circe.Encoder

//Http4s imports
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._

object HttpServer {

  //Create an Http4s decoder from our Decoder[ValidatedNec[String, Account]]
  given EntityDecoder[IO, ValidatedNec[String, Account]] = accumulatingJsonOf[IO, ValidatedNec[String, Account]]

  val service = HttpRoutes.of[IO] {

    case request =>
      request.as[ValidatedNec[String, Account]] //Convert our request into a ValidatedNec[String, Account]
        .handleErrorWith(IO.raiseError) //Raise eventual exceptions
        .map(_.asJson) //Convert the result into JSON
        .flatMap(Ok(_)) //Create a "OK" request from our JSON
  }
}
```

You now can [test it](https://github.com/Iltotore/iron-cats-example#test-the-project)

Our example is now functional. If we send
```json
{
  "username":"Iltotore",
  "email":"me@myemail.com",
  "password":"Abc123"
}
```

We will receive
```json
{
  "Valid": {
    "username": "Iltotore",
    "email": "me@myemail.com",
    "password": "Abc123"
  }
}
```

And if we illegal values:
```json
{
  "username": "Iltotore",
  "email": "memyemail.com",
  "password": "abc123"
}
```

We'll receive:
```json
{
  "Invalid": [
    "memyemail.com should be an email",
    "abc123 should contain at least an upper, a lower and a number"
  ]
}
```

## Routing

We can match on a specific request/route in the `HttpRoutes.io` block in our service. Let's only accept POST requests to `/register` using Http4s' DSL:

```scala
val service = HttpRoutes.of[IO] {
  case request@POST -> Root / "register" =>
    request.as[RefinedFieldNec[Account]]
      .handleErrorWith(IO.raiseError)
      .map(_.asJson)
      .flatMap(Ok(_))

  case unknown =>
    NotFound()
}
```

# Use Iron in the project
We created a functional API but everytime we need to parse a specific value (like an email), we have to apply the validation method and this quickly become boilerplaty.

Iron allows to attach constraints to types. The value returned is an `Either[IllegalValueError[A], A]` where `A` is the original type.

Iron comes with a support for Cats and Circe by providing Cats Validated support and Circe Decoders/Encoders for constraints.

Let's see how our `createAccount` method looks like with Iron.

Firstly, we add type aliases to make our constraints more readable:
```scala
type Username = String ==> Alphanumeric
type Email = String ==> (Match["^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$"] DescribedAs "Value should be an email")
//At least one upper, one lower and one number
type Password = String ==> (Match["^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z\\d]+$"] DescribedAs
  "Value should contain at least an upper, a lower and a number")
```

and now, here our new `createAccount` method:
```scala
def createAccount(username: Username, email: Email, password: Password): RefinedFieldNec[Account] = (
  username.toField("username").toValidatedNec,
  email.toField("email").toValidatedNec,
  password.toField("password").toValidatedNec
).mapN(Account.apply)
```
- `RefinedFieldNec[A]` is an alias for `ValidatedNec[IllegalValueError.Field, A]`
- `toField(String)` converts the potential value-based IllegalValueError[A] contained by our constrained type to a field-based IllegalValueError.Field.
- `toValidatedNec` (provided by cats) converts our constrained type to an accumulative Validated. See [Cats page on Validated](https://typelevel.org/cats/datatypes/validated.html) for further information.

For more information about Iron, check the [Github page](https://github.com/Iltotore/iron)

And that's all! There is no other required step to include Iron in your project as it is easily translatable into `Either` or `Validated`.

We've seen in this tutorial the extreme readability of our small API offered by Iron and Cats can consult the code and test instructions of the finished example on https://github.com/Iltotore/iron-cats-example

Links:
- Iron: https://github.com/Iltotore/iron/
- Cats: https://typelevel.org/cats/
- Circe: https://circe.github.io/circe/
- Http4s: https://http4s.org/