ZIO CRUD Rest Microservice

How to build a Scala Zio CRUD Microservice

1. Overview

This tutorial will introduce how to build from scratch, a REST microservice using the ZIO framework, and examples of ZIO dependency injection, ZIO HTTP, JSON, JDBC, and others from the ZIO environment. source code be found in our repository

2. What's ZIO

ZIO is a next-generation framework for building cloud-native applications on the JVM. With a beginner-friendly yet powerful functional core, ZIO lets developers quickly build best-practice applications that are highly scalable, testable, robust, resilient, resource-safe, efficient, and observable.

At the heart of ZIO is a powerful data type called ZIO, which is the fundamental building block for every ZIO application.

The ZIO data type is called a functional effect, and represents a unit of computation inside a ZIO application.

The type parameters of the ZIO data type have the following meanings:

  • R - Environment Type. The environment type parameter represents the type of contextual data that is required by the effect before it can be executed. For example, some effects may require a connection to a database, while others might require an HTTP request, and still others might require a user session. If the environment type parameter is Any, then the effect has no requirements, meaning the effect can be executed without first providing it any specific context.
  • E - Failure Type. The failure type parameter represents the type of error that the effect can fail with when it is executed. Although Exception or Throwable are common failure types in ZIO applications, ZIO imposes no requirement on the error type, and it is sometimes useful to define custom business or domain error types for different parts of an application. If the error type parameter is Nothing, it means the effect cannot fail.
  • A - Success Type. The success type parameter represents the type of success that the effect can succeed with when it is executed. If the success type parameter is Unit, it means the effect produces no useful information (similar to a void-returning method), while if it is Nothing, it means the effect runs forever, unless it fails.

3. Writing ZIO Services

ZIO recommends writing services using the Service Pattern, which is very similar to the object-oriented way of defining services. It uses scala traits to define services, classes to implement services, and constructors to define service dependencies. Finally, it lifts the class constructor into the ZLayer.

4. HTTP Routes (Rest integration)

ZIO also provides ZIO HTTP, which allows exposing over HTTP the API that has been defined and implemented.

                                
import com.thesampleapp.crud.zio.domain.{User, UserRepository}
import zio.ZIO
import zio.http._
import zio.http.model.{Method, Status}
import zio.json._
object UserRoutes {
  def apply(): Http[UserRepository, Nothing, Request, Response] = Http.collectZIO[Request] {
    case Method.GET -> !! / "users" => UserRepository.
      findAll.map(response => Response.json(response.toJson)).orDie

    case Method.GET -> !! / "users"/ userId =>
      UserRepository.findById(userId).map{
        _ match {
          case Some(user) => Response.json(user.toJson)
          case None => Response.status(Status.NotFound)
        }
      }.orDie
    case req @ Method.POST -> !! / "users" => {
      for{
      user <- req.body.asString.map(_.fromJson[User])
      r <- user match{
        case Left(e) =>
          ZIO
            .debug(s"Failed to parse the input: $e")
            .as(
              Response.text(e).setStatus(Status.BadRequest)
            )
        case Right(u) =>
          UserRepository
            .save(u)
            .map(id => Response.text(id))
      }
    } yield r}.orDie

    case Method.DELETE -> !! / "users"/ userId => {
      for{
        _ <- UserRepository.delete(userId)
      }yield ()
    }.map(_ => Response.status(Status.NoContent)).orDie
  }
}
                                
                            

5. ZIO Application

Finally, the ZIO APP allows the start of the ZIO application and configuration of the application, here the routes and services are being provided using the ZIOAppDefault, by the overriding run method.

                                
import com.thesampleapp.crud.zio.http.UserRoutes
import com.thesampleapp.crud.zio.infra.InMemoryUserRepository
import zio._
import zio.http._

object ZioApp extends ZIOAppDefault{
  override val run =
    Server.serve(UserRoutes()).provide(Server.default,InMemoryUserRepository.layer)
}