http server: node.js, express, zio, error channels, mixed code base

http server: node.js, express, zio, error channels, mixed code base

This is a quick blog on integrating zio into node.js+express and scala.js. It also shows you how to align the zio error channel with the semantics of the domain. An explicit goal is to enable scala.js and zio in a mixed code base.

You would normally create a http server using one of the many scala+jvm based libraries. play, cask, akka-http or http4s. They are all good and integrate well with scala. Most of them allow you choose the underlying java http server library. The scala layer is typically an API layer on top. I like the idea that cask is focused on making it quick and simple to build websites even although cask is not asynchronous. Allowing you to start small and quick then incrementally build out your server’s logic is a good way to get programmers started and allow them to grow their usage as their understanding of the API increases. node.js+express is like that. Although it is easy to get started with node.js+express, node.js+express is flexible and has a rich ecosystem of plugins. Perhaps more importantly, developers of different stripes can work together since many programming language transpilers target the js runtime.

With a small, flexible API, it is easy to use for a quick and simple server. If your dependencies are only available for express, using scala.js with express can be a good option to leverage your scala.js skills. For example, I was looking at integrating oauth2 into the server and using jvm libraries looked incredibly painful. There was an easy to use express passport library from the oauth2 provider that took about 2 lines of configuration to add to the server. In contrast, I was left scratching my head about gluing the authentication library into the jvm-based http servers. Many times, the scala facades do not assist with such a low level concern but assume you can synchronously produce an access token.

Here’s the gist of this blog:

  • Assume there is an existing node.js typescript/js based express server.
  • Assume it is okay to use a mixed code base with scala.js being the smaller portion.
  • Handlers should be expressed as zio tasks.
  • Ensure that error management works regardless of programming language.
  • Use zio: Error channel, effects, and errors aligned withe HTTP domain.

The formulation below helps illustrate how to gradually introduce scala.js into an express application without having to force the entire application to be written in scala.

zio error channel

express has a few ways to communicate errors. You can:

  • Throw an error in a synchronous handler and hope that a handler downstream handles it, or, the default express handler will handle it in some way.
  • Call next(err) with the error either in synchronous code or asynchronous code.

Error managment feels a bit messy in express but it was designed to allow you to code a http server quickly using different error handling strategies. It is, I admit, amazingly easy to use express to create a server. However, it can get confusing about where errors are handled. The general rule I use for javascript-only-codebases is that for asynchrous handlers (they should all be asynchronous), trap all the errors in the handler and do not rely on downstream error handlers. That is, compose every handler to be as self-contained as possible to improve “reasoning about your code.” However, in a mixed code base, you probably want to be flexible and allow both different languages to have consistent error handling. This implies that a separate error handler may be an important. In the approach below for scala.js, we will code the integration layer to give you a choice.

We can use the zio error channel to model handler errors. Assuming we are building a REST-like server, we want our effects to return an E that looks like:


trait HTTPError extends js.Object {
  val status: Int
  val message: String
  var `type`: js.UndefOr[String] = js.undefined
  var innerError: js.UndefOr[js.Object] = js.undefined
}

object HTTPError {
  def apply(s: Int, m: String) = new HTTPError { val status = s; val message = m }
}

That’s about as simple as it gets. The error is not related to Throwable. You may choose to handle errors using an express error handler that is added to the end of the handler list. Here’s a typescript verson:

/** Global error handler. Translates non-RESTError errors into generic 500. */
export function globalRESTErrorHandler(err:  Error, req:  Request, resp:  Response, next:  NextFunction) {
	if (resp.headersSent) next(err)
	else if (err  instanceof  RESTError) setRESTError(resp, err)
	else {
		// we don't know the details, so general 500 error
		resp.status(500).json({
		error:  "Server side error occurred",
		innerError:  err
		})
	}
}

we add it to the “app” server via

// all other handlers
// ...
app.use(globalRESTErrorHandler)

Now, js or scala.js REST errors are consistently returned to the client. The alternative approach is that we could build explicit error handling into the zio effects. I typically install a handler like this even though I prefer to handle all errors in the handler itself whether it is javascript or scala. If you expose this javascript handler to scala.js throuh a JSImport you could use this in your zio effect.

express types

We need to define a few express related types. We could use some of the existing scala.js facade libraries but it only takes a few minutes to make a facade that is good enough for our use–this faced took about about 15 minutes.

package object express {
  type Next = js.Function1[js.UndefOr[_ <: js.Any|String], Unit]
  type TotalHandler = js.Function2[Request, Response, Unit]
  type Handler = js.Function3[Request, Response, Next, Unit]
  type ErrorHandler[E <: js.Object] = js.Function4[E, Request, Response, Next, Unit]
  type PathParams = String | js.RegExp | Array[String|js.RegExp]
}

These facades are cut-down from the full API but they illustrate the approach. Note that I do not define an express “app” type because the main http server is running in typescript.

@js.native
trait Request extends js.Object {
  def params[T <: js.Object]: T = js.native
  def asJsonObject[T <: js.Object]: T = js.native
  val baseUrl: String = js.native
  val originalUrl: String = js.native
  val path: String = js.native
  val protocol: String = js.native
  val secure: Boolean = js.native
  def bodyJson[T <: js.Object]: T = js.native
  val cookies: js.Dictionary[js.Object] = js.native
  def query[T <: js.Object]: T = js.native
  val hostname: String = js.native
  val ip: String = js.native
  val fresh: Boolean = js.native
  val stale: Boolean = js.native

  def accepts(): js.Array[String] = js.native
  def accepts(t: String): String | Boolean = js.native
  def accepts(t: js.Array[String]): String | Boolean = js.native

}

@js.native
trait Response extends js.Object {
  val headersSent: Boolean = js.native
  val locals: js.Object = js.native
  val app: App = js.native
  def status(v: Int): Response = js.native
  def json(v: js.Any): Unit = js.native
  def send(v: String|js.Object|js.Array[js.Any]): Unit = js.native
  def sendStatus(s: Int): Unit = js.native
  def set(k: String, v: String): Unit = js.native
  def get(f: String): String = js.native
  @JSName("append")
  def appendMany(f: String, values: js.Array[String]): Unit = js.native
  def append(f: String, v: String): Unit = js.native  
  def end(): Unit = js.native
  def attachment(a: String): Unit = js.native
  @JSName("attachment")
  def attachments(a: js.Array[String]): Unit = js.native
  def location(path: String): Unit = js.native
  def render(view: String, locals: js.Array[js.Any]): Unit = js.native
  def sendFile(path: String, options: js.Object, cb: js.Function2[Request, Response, js.Function1[_ <: js.Any, Unit]]): Unit = js.native
}

@js.native
trait Routable extends js.Object {
  def useTotal(handler: TotalHandler): Unit = js.native
  @JSName("use")
  def useOnPathTotal(p: PathParams, handler: TotalHandler): Unit = js.native
  @JSName("use")  
  def useOnPathWithManyTotal(p: PathParams, handlers: js.Array[TotalHandler]): Unit = js.native

  def use(handler: Handler): Unit = js.native
  @JSName("use")
  def useOnPath(p: PathParams, handler: Handler): Unit = js.native
  @JSName("use")  
  def useOnPathWithMany(p: PathParams, handlers: js.Array[Handler]): Unit = js.native
}

zio infrastructure

A small amount of zio infrastucture integrates “good enough” with express. To send data back, most express handlers call mutable methods on the Response object. Contextual request-level data is usually added to th Request object. Its hard to know what changes have been made to either the Request or Response object inside your handler. For example, it is common to attach a Request-level “environment” to the Request object. A common pattern is to add an “user” record based on a lookup in a user database or based on an authentication library like Passport. If we are FP oriented, we would not want to alter the Response object in our handler and let any mutation occur at the “edge” of the zio effects world. Fortunately, we can easily code up a simple, immutable “response builder.”

Each handler takes a Request and Response object, so a basic handler looks like (request, response, next) => .... Both th Request and Response objets are obvious candidates to be includd in the zio enviromnent that a “task handler” relies on. If each handler returns a ResponseBuilder, we could exclude the Response object from the zio environment. However, given that an upstream handler may attach “context” to the Response object for some reason, I have included the Response object in the zio enviroment definition below. If we wanted to hide the mutability of the Response object, we could alter the definition of the Response object in the zio environment so that mutable methods are unavailable. If we wanted to hide information in the Request object and build a general API, we could add a module to the zio environment that returns the necessary data and behind the scenes access the Request object. This approach would abstract how request-level information is propagated through the handler chain.

To keep it simple, we will just show the Request-Response service and RequestBuilder. In other scenarios, you may need to create handlers with access to other “scopes” such as the “application/global” scope. To access this scope, you could define your handler to use currying prior to adding it into the express application. Of course, you can just use the zio environment and assume your zio runner will hand you the scope.

In other words, the zio environment decreases for mutable Request objects. In express, the “app” instance is readily available through the Request object so you could also easily access “global” scope. Hopefulyl it is clear that making dependencies available to the handler is an important consideration. The lifetime and scope of those dependencies may dictate the specific approach you use… The zio environment is one solution that addresse these needs.

First, we will include both the express Request and Response in the environment by creating an Express module:

trait Express {
  def express: Express.Service
}

object Express {
  trait Service {
    def request: Request
    def response: Response
  }

  def request = ZIO.access[Express](_.express.request)
  def response = ZIO.access[Express](_.express.response)

  case class DefaultService(request: Request, response: Response) extends Service
}

Here’s our zio environment:

case class Env(express: Express.Service)
    extends Clock.Live with Console.Live with System.Live with Random.Live

Our ResponseBuilder transforms the mutable Response object in express to a mutable one. In the design below, we need to replay the builder against the Response object at the end of the world.

case class ResponseBuilder(run: Response => Response) { subject =>
  def underlying(next: Response => Response) = ResponseBuilder(run andThen next)
  def status(s: Int) = subject.underlying{r => r.status(s); r}
  def json(v: js.Any|String) = subject.underlying{r => r.json(v.asInstanceOf[js.Any]); r }
  def header(k: String, v: String) = subject.underlying{r => r.set(k,v); r}
}

object ResponseBuilder {
  def apply(s: Int) = new ResponseBuilder(_.status(s))
}

Since express using handles to describe route logic, we need to define a handler that takes a zio effect but returns the javascript version of the handler. We also need middleware that looks for an effect and runs it automatically. In express, a handler is function that takes only a Request an Response object and an optional 3rd argument, called next, that allows you to signal an error, skip handlers in a handler chain (array of handlers) or skip processing in that handler altogether. We are choose to separate the “attach a zio effect to a Request object” from the “run any effects attached to a Request object” into two separate functions.

Calling next() in a handler allows other downstream handlers to be used. If you do not call next() it is assumed that your handler handled the Request and no other handlers are called. There is another “middleware” signature used for error handlers that has 4 arguments (error, request, response, next) => .... Hence, depending on the parameter count, a “middleware” handler could be a simple handler that must handle the entire response (less error handling if you throw an error), a handler that can call next with an error or to signal to process downstream handlers or an error handler. If you code a simpler handler but fail to call one of the “send” methods, your server will hang. If you receive the next parameter in your hanlder and forget to call next after your handlr passes through the Request, your server will hang. Lovely! We can completely eliminate this confusion using effects.

For example, we could code:

  • ZIO[Env, Nothing, Unit]: Indicating that the task handler handles all erorrs and handles all “sending” of values.
  • ZIO[Env, HTTPerror, T]: Indicating that this task handler could return an HTTPError or a value and something else needs to return content to the client.
  • ....: Some other combination.

Here’s all we need assming that a task handler could return an error or a value and that something else is responsible for sending the content back to the client:

trait Infrastructure[En] {
  val rts = (new DefaultRuntime { }).withReportFailure{
    // if debugging, print something out, otherwise, let error handling work
    case x => ()//println(s"Failure occurred while processing effect: ${x.prettyPrint}")  
  }

  def zioHandler(effect: ZIO[_ :> En, HTTPError, ResponseBuilder]): Handler =
    (request, response, next) => {
      val d = response.asInstanceOf[js.Dynamic]
      d.effect = effect.asInstanceOf[js.Any]
      next(js.undefined)
    }

  /**
   * @param mkEnv Make environment needed for the effect.
   * @param onError If E failure happens, determine how to respond back.
   * @param onRTSError non-E error happens, determine how to respond back.
   */
  def makeEffectRunner[E <: js.Object](
    mkEnv: (Request, Response) => En,
    onError: (E, Response, Next) => Unit,
    onRTSError: (String, Response, Next) => Unit,
  ): Handler =
    (req, resp, next) => {
      val d = resp.asInstanceOf[js.Dynamic]
      val effect_opt = d.effect.asInstanceOf[js.UndefOr[ZIO[En, E, ResponseBuilder]]]
      effect_opt.fold{next(js.undefined)}(effect => {
        val env = mkEnv(req, resp)
        rts.unsafeRunAsync(effect.provide(env)) {
          case Exit.Success(builder) =>
            d.effect = js.undefined
            builder.run(resp)
          case Exit.Failure(cause) =>
            d.effect = js.undefined
            cause.failureOption match {
              case Some(e) => onError(e, resp, next)
              case _ => onRTSError(cause.prettyPrint, resp, next)
            }
        }
      })
    }
}

// ...
object UseThisInApp {
  val infrastructure = new Infrastructure[Env]{}
  import infrastucture._

  @JSExportTopLevel("zioHandler")
  val handler = zioHandler _
  
  @JSExportTopLevel("effect_runner")
  val effect_runner_split_personality = makeEffectRunner[Env, HTTPError](
    (q, p) => Env(DefaultService(q,p)),
    (e, r, next) => r.status(500).json(new HTTPError { val message = e.message; val status = 500 }),
    (errmsg, _, next) => next(new HTTPError{ val message = errmsg; val status = 500 })
  )
 }

effect_runner_split_personality is a specific handler that converts errors into HTTPErrors. It has a split personality because effects that directly generate an error are returned directly to the caller via “send” methods, but RTS errors are handeled by calling next and require a global handler to be installed.

Our general effect runner, makeEffectRunner, has error handler functions as parameters that take Response and next so you can customize how the error is communicated. Since we are assuming that scala.js is just part of the solution, we may need to rely on downstream handlers that have already been setup.

The makeEffectRunner is a bit generic, not overly abstract, in how it handles errors. There are two types of errors. Errors that are generated from our effect and are of type HTTPError. Its also possible the runtime fails with some other error. Given a Cause object we can extract out the actuall HTTPError failure using cause.failureOption. If the effect did not fail in a predictable manner, then we need to still generate an error. By providing the Response object or the next function, you can create a variety of effect runners to integrate into your existing application.

Our makeEffectRunner also allows us to control the scope of dependencies since it also takes a function to build the environment. Since we have the Request object available, we can make Request level data available. We could also make “app” level data available, or we could use global variables that are floating in the ether whatever they are. By bundling the express handler and the handler-as-effect-runner together, we ensure that any effects we create are matched to the handler that runs them.

use it

To use this in express, we can add the effect handler to the chain of handlers. We illustrate how this looks in javascript although this could all be done in the init method which is exported from scala:

// we named our output scala file scala-server. es-module output format.
import { effect_runner_split_personality, init } from "scala-server"
// ...
init(app) // scala call, see below
app.use(effect_runner_split_personality) // could do this in init
app.use(globalRESTErrorHandler) // pure javascript

Per the description in the previous section, we separated out declaring our paths and the handler that runs the effects. You could also run the effect inside each handler directly.

To add our scala.js defined routes, I like to have the init function add the routes in the scala.js world so that the scala types are used:


import Express._
object Queries {
  @JSExportTopLevel("init")
  def init(router: Routable): Unit = {
    println("Initializing scala handlers.")
    router.useOnPath("/test", zioHandler(effect))
    router.useOnPath("/testfail", zioHandler(effect_fail))
    router.useOnPath("/testfail1", zioHandler(effect_fail1))
    //router.useOnPath("/testfailthrow", zioHandler(effect_fail2))
  }

  val effect =
    ZIO.environment[Env].flatMap{_ =>
      ZIO.succeed(ResponseBuilder(201).json("YEAH!"))
    }
    
  // standard query fetching a list from a database  
  val companies: ZIO[Env, HTTPError,ResponseBuilder] =
    (for {
      env <- ZIO.environment[Env]
      req <- request
      resp <- response
      pool <- dbPool
      result <- (pool.request().query(companyQuery("", 1000)) pipe jsPromiseToZIO)
    } yield result)
      .map( result => ResponseBuilder(200).json(ListResponse(data.length, result.data.map(adjust(_)))))
      .catchAll{ e => ZIO.fail(HTTPError(500, s"Error obtaining company list: ${e.getMessage}")) }

  val effect_fail =
    ZIO.environment[Env].flatMap{ env =>
      // Return error by failing the effect
      ZIO.fail(HTTPError(404,s"Person not found called from url ${env.express.request.path}"))
    }
   
  val effect_fail1 =
    for {
      env <- ZIO.environment[Env]
      r <- request
      // return error via ResponseBuilder
      rb = ResponseBuilder(404).json(s"Person not found called from url ${env.express.request.path}")
    } yield rb

  val effect_fail2 =
    ZIO.environment[Env].flatMap {_ =>
      ZIO(throw new IllegalArgumentException("blah"))
    }
}

Notcie that if we tried to add an effect that returns a non-HTTPError error, we get a compile message:

[error]  found   : zio.ZIO[express.Env,Throwable,Nothing]
[error]  required: zio.ZIO[express.Env,express.HTTPError,express.ResponseBuilder]
[error]     router.useOnPath("/testfailthrow", zioHandler(effect_fail2))
[error]                                                   ^
[error] one error found

This shows us that we need to ensure that our error channel is purely HTTPError which is not derived from Throwable. Inside each effect, we will need to convert any errors in java/scala land to the simple HTTPError to be returned to the server.

If its a simple http server you may not need to use zio and scala.js but after programming in typescript for awhile, scala.js is alot more clear.

That’s it!

Comments

  1. Hey this is really very useful information you are sharing here.Thanks for sharing
    Node JS Online training
    Node JS training in Hyderabad

    ReplyDelete
  2. Excellent Information Thank you..

    ELearn Infotech offers Real-time Node JS Training in Hyderabad. Our Node JS course includes from Basic to Advanced Level Node JS Concepts. We have designed our Node JS course content based on students Requirement to Achieve Goal. We offer both Node JS class room training in Hyderabad and Node JS Course online training by 10+ years Realtime Experts.

    ReplyDelete

Post a Comment

Popular posts from this blog

quick note on scala.js, react hooks, monix, auth

zio environment and modules pattern: zio, scala.js, react, query management

user experience, scala.js, cats-effect, IO