Reference

Architecture

An effect-free native layer, a Kyo core, a hand-written ZIO adapter, optional schema and storage bridges, and your application.

h34s is a toolbox for running Scala.js server code in your Nuxt/Nitro application. You add the modules you need, write your models and services, and mount your handlers as Nuxt routes. The repository also holds a runnable demo in examples/nuxt/ and the guides and the documentation site in docs/. Neither is part of the toolbox.

Status. This page describes the design of 0.2.0-alpha.1. The older 0.1.0-alpha.1 release has a different module graph with two independent runtimes; the migration guide lists every change.

Layers

One runtime does the hard work. The h3 transport, the request lifecycle, cancellation, streaming, error rendering, configuration access and application-scoped resources are implemented once, in Kyo. The ZIO API is a hand-written adapter over that core.

Layersbt project / artifactWhat it gives youDepends on
Native layerh3Ext / h3-extPure values shared by every runtime: EventHandler, Request, RequestBody, BodyLimit, ResponseHead, Status, SetCookie, OnFailure, HandlerOptions, SafeError, HttpError, InvalidInput, ErrorEntry, ConfigError, Secret, NativeConverter[Unit]. The h3, Node and Nitro facades are internal.native-converter. No effect library.
Kyo corekyoH3Ext / kyo-h3-extKHandler.define, the request lifecycle, body reading with limits, Response, error rendering, diagnostics, RuntimeConfig, Shared, Response.sendStream and sendEventsh3-ext, kyo-core 1.0.0-RC7
ZIO adapterzioH3Ext / zio-h3-extZHandler.define, Response over ZState with sendStream and sendEvents, ZRuntimeConfig, ZShared, and the awaiting ZIO-to-Kyo bridgekyo-h3-ext, kyo-zio, ZIO 2.1.26, zio-prelude, zio-json, native-converter-zio-ext
Kyo Schema bridgenativeConverterKyoSchemaExt / native-converter-kyo-schema-extderives Schema, NativeSchema for Kyo Schemah3-ext, kyo-schema
ZIO Schema bridgenativeConverterZIOSchemaExt / native-converter-zio-schema-extderives Schema, NativeSchema for ZIO Schemah3-ext, zio-schema 1.7.5
ZIO convertersnativeConverterZIOExt, nativeConverterZIOJsonExtNonEmptyList and zio-json Json converters, eimaph3-ext, ZIO
StoragekyoNuxtHubExt / kyo-nuxthub-ext, zioNuxtHubExt / zio-nuxthub-extKHubKv.mount, ZHubKv.mount over Nitro storageKyo core; the ZIO one also needs the ZIO adapter
npm tooling@yuri4n/h34s-nuxtBuild CLI, Nuxt module, development loading, source maps, and the Nitro plugin that supplies flags, runtime config, storage and the close hookNode, Nuxt. No Scala dependency.
Your applicationyour sbt project and Nuxt appModels, services, AppHandler, resources, route mounts, database and deploymentthe modules you choose

Dependencies point one way. Kyo modules never depend on ZIO. ZIO modules get Kyo transitively.

h3-ext                                 (no effect library)
├── kyo-h3-ext                         (Kyo core)
│   ├── kyo-nuxthub-ext
│   └── zio-h3-ext                     (also uses native-converter-zio-ext, kyo-zio, ZIO)
│       └── zio-nuxthub-ext            (also uses kyo-nuxthub-ext)
├── native-converter-kyo-schema-ext    (also uses kyo-schema)
├── native-converter-zio-schema-ext    (also uses zio-schema)
└── native-converter-zio-ext           (also uses ZIO)
    └── native-converter-zio-json-ext

Each module depends on its parent. A build check during the refactor found no dev.zio artifact on the Kyo core's compile classpath.

Pick the smallest set for your application:

  • A Kyo application adds kyo-h3-ext. Add native-converter-kyo-schema-ext for schemas and kyo-nuxthub-ext for storage.
  • A ZIO application adds zio-h3-ext. Add native-converter-zio-schema-ext for schemas and zio-nuxthub-ext for storage.
  • Every application adds the npm package @yuri4n/h34s-nuxt and the runtime dependency h3@1.15.4.

Follow one request

  1. Nitro routes the request to a linked Scala.js module. The h34s Nuxt module registered that module for the route that your handler declares with .at(Route...); h34s-scala read the route after linking and wrote it into the output's route manifest. A one-line route file in server/api/ that re-exports the module works too.
  2. The h34s Nitro plugin has attached a frozen record to event.context.h34s: the development and debug flags, the request's runtime config, and Nitro storage. Requests cannot change this record.
  3. KHandler.define takes one immutable Request snapshot. It runs your function Request => B < HandlerEffects once, on its own fiber, in a request Scope, with a Var[ResponseHead] that starts empty.
  4. Your program reads the body, calls your services, sets status, headers and cookies with Response.*, and returns the body.
  5. When the program and its finalizers have finished, the core writes the response once. On success it applies the handler defaults, then everything the program set. On failure it applies the failure rule. It renders the body with the handler's HandlerOptions.
  6. If the client disconnects first, the core interrupts the program, waits for its cleanup, and writes nothing.

ZHandler.define adds one step. It runs your ZIO program in one ZIO fiber per request, on the given ZIO Runtime, with a ZState[ResponseHead] and a ZIO Scope. It then hands the result and the head to the core, which does steps 5 and 6. Interrupting a request interrupts the ZIO fiber and waits for its finalizers.

Where your code goes

The functional-design layers map to code like this:

LayerWhat lives thereWho owns it
Business logicYour services and programs, as Kyo A < S or ZIO effectsYou
InterfacesYour service traits (Greetings, Notes) and the h34s values (Request, SafeError, Secret)You and h34s
ImplementationThe h34s runtime, its internal facades, and your service interpreters (NotesLive)h34s and you
ApplicationYour AppHandler: configuration, then Shared/ZShared resources, then Env/ZLayer wiringYou

AppHandler is a name the demo uses for this one wiring point. It is your code, not an h34s export. Call KHandler.define or ZHandler.define directly when you do not need a wrapper.

Errors and diagnostics

A handler is an error boundary. Only a SafeError that your program fails with through the typed channel reaches the client, with its status and entries. Everything else becomes an opaque 500. Diagnostics appear only in Nuxt development mode or in debug mode. See Kyo handlers, ZIO handlers and debugging.

A finalizer that fails behaves the same in both runtimes: the failure is logged on the server, and the response that the program computed does not change. The log messages differ. Kyo 1.0.0-RC7 logs Scope finalizer failed through its Log. The ZIO adapter writes h34s: a ZIO finalizer failed, with Cause.prettyPrint, through console.error, for value, Unit and streamed bodies and on a client disconnect. Since the next release (provisionally 0.3.0-alpha.1), a dying ZIO finalizer no longer turns a value or Unit response into a 500.

What h34s does not do

  • Infer TypeScript types from Scala classes. The generated declarations describe what each converter writes; a converter that does not describe itself gives unknown. See routes and typed $fetch.
  • Supply a database client, a connection pool or migrations. Use kyo-sql or Skunk directly; see database.
  • Package or deploy your application. Nix and sbt-native-packager support are planned.
  • Run anywhere but Node. Streaming is tested over Node HTTP responses and over a copy of Nitro's in-process local fetch; other Nitro presets are untested.

Source locations

PathWhat to read
scala-modules/h3The native layer and its tests
scala-modules/kyo-h3The Kyo core and its tests
scala-modules/zio-h3The ZIO adapter and its tests
scala-modules/native-converter-kyo-schema, native-converter-zio-schemaSchema bridges
scala-modules/native-converter-zio, native-converter-zio-jsonZIO converters
scala-modules/kyo-nuxthub, zio-nuxthubStorage
packages/nuxtCLI, Nuxt module, Nitro plugin
examples/nuxt/scala/kyo, examples/nuxt/scala/zioThe demo's Kyo and ZIO applications
docs/The guides and this documentation site

examples/zio-migrations is an unpublished example that derives Skunk codecs and PostgreSQL DDL from ZIO Schema. It is not a supported API.

Next coherent slices

Routes declared in Scala and the .d.ts declarations for typed $fetch are on main and ship with the next release; see routes and typed $fetch. The next slices are:

  1. The planned Nix and sbt-native-packager paths over the same build pipeline.
  2. Interactive documentation: compiler-derived types, symbol links and runnable examples.