Generating a Server

guardrail-generated servers come in two parts: a Resource and a Handler. The Resource contains all the routing logic, accepting a Handler as an argument to the route function in order to provide an HTTP service in whichever supported HTTP framework you’re hosting your service in.

The following is an example from the http4s server generator:


// The `Handler` trait is fully abstracted from the underlying http framework. As a result, with the exception of some
// structural alterations (`F[_]` instead of `Future[_]` as the return type) the same handlers can be used with
// different `Resource` implementations from different framework generators. This permits greater compatibility between
// different frameworks without changing your business logic.
  
trait UserHandler[F[_]] {
  def createUser(respond: CreateUserResponse.type)(body: definitions.User): F[CreateUserResponse]
  def createUsersWithArrayInput(respond: CreateUsersWithArrayInputResponse.type)(body: Vector[definitions.User]): F[CreateUsersWithArrayInputResponse]
  def createUsersWithListInput(respond: CreateUsersWithListInputResponse.type)(body: Vector[definitions.User]): F[CreateUsersWithListInputResponse]
  def deleteUser(respond: DeleteUserResponse.type)(username: String): F[DeleteUserResponse]
  def getUserByName(respond: GetUserByNameResponse.type)(username: String): F[GetUserByNameResponse]
  def loginUser(respond: LoginUserResponse.type)(username: String, password: String): F[LoginUserResponse]
  def logoutUser(respond: LogoutUserResponse.type)(): F[LogoutUserResponse]
  def updateUser(respond: UpdateUserResponse.type)(username: String, body: definitions.User): F[UpdateUserResponse]
}

class UserResource[F[_]](mapRoute: (String, Request[F], F[Response[F]]) => F[Response[F]] = (_: String, _: Request[F], r: F[Response[F]]) => r)(implicit F: Async[F]) extends Http4sDsl[F] with CirceInstances {
  def routes(handler: UserHandler[F]): HttpRoutes[F] = HttpRoutes.of {
    {
      case req @ POST -> Root / "v2" / "user" =>
        mapRoute("createUser", req, {
          req.decodeWith(createUserDecoder, strict = false) { body => 
            handler.createUser(CreateUserResponse)(body) flatMap ({
              case CreateUserResponse.Ok =>
                F.pure(Response[F](status = org.http4s.Status.Ok))
            })
          }
        })
      case req @ POST -> Root / "v2" / "user" / "createWithArray" =>
        mapRoute("createUsersWithArrayInput", req, {
          req.decodeWith(createUsersWithArrayInputDecoder, strict = false) { body => 
            handler.createUsersWithArrayInput(CreateUsersWithArrayInputResponse)(body) flatMap ({
              case CreateUsersWithArrayInputResponse.Ok =>
                F.pure(Response[F](status = org.http4s.Status.Ok))
            })
          }
        })
    }
  }
  `...`
}

As all parameters are provided as arguments to the function stubs in the trait, there’s no concern of forgetting to extract a query string parameter or introducing a typo in a form parameter name.

The routes and resources generated by guardrail can be hooked up into your HTTP4s server like so:

import org.http4s.ember.server.EmberServerBuilder

class UserImpl extends UserHandler[IO] { /* Your code here */ }
val userHandler: UserHandler[IO] = new UserImpl
val usersService = new UsersResource[IO]().routes(userHandler)
val httpApp = usersService.orNotFound

// Same basic server setup as in the http4s quickstart
EmberServerBuilder.default[IO]
  .withHttpApp(httpApp)
  .build
  .use(_ => IO.never)
  .as(ExitCode.Success)

(See it in action: guardrail-dev/guardrail-sample-http4s, guardrail-dev/guardrail-sample-sbt-http4s-zio)

Separation of business logic

Providing an implementation of a function with a well-defined set of inputs and outputs is natural for any developer. By reducing the scope of the interface a developer writes against, implementations are more clear and concise.

Furthermore, by providing business logic as an implementation of an abstract class, unit tests can test the routing layer and business logic independently, by design.

API structure slip is impossible

As parameters are explicitly provided as arguments to functions in Handlers, any alteration to parameters constitute a new function interface that must be implemented. As a result, if providing an implementation for an externally managed specification, the compiler informs when a previously written function is no longer sufficient.

By representing different response codes and structures as members of a sealed trait, it’s impossible to return a structure that violates the specification, even for less frequently used response codes.

Finally, describing an endpoint in your specification without providing an implementation for it is a compiler error. This prevents reduction of functionality due to refactors, human error, or miscommunication with other teams.

Prev: Sample API specification Next: Generating clients