Guide

Adoption paths

Add h34s to a new Nuxt app, an existing Nuxt app, or an existing Scala.js project.

h34s is a toolbox. You add a Scala.js server module to a Nuxt application, write handlers in Kyo (or ZIO), and mount them as Nuxt routes. Your models, services, configuration and resources stay in your application.

Pick the path that matches what you have:

  1. Start a new project
  2. Add Scala handlers to an existing Nuxt app
  3. Add Nuxt to an existing Scala.js project

Every path ends with the same steps: write a handler, mount it, run it.

Prefer a coding assistant? Give it the prompt in Set up h34s with a coding assistant; it follows the steps on this page. For path 3, the prompt uses the 0.2.0-alpha.1 layout, with the Nuxt application at the root of your sbt build.

Get the libraries

The API in these guides is 0.2.0-alpha.1. The older 0.1.0-alpha.1 has a different API; see the migration guide.

Install 0.2.0-alpha.1 from GitHub Packages with a GitHub token, as Releases describes. Its release workflow published the packages and verified them through the registry.

To build against h34s source changes that are not released, use one of these:

  • Publish an h34s checkout to your local Ivy repository: run sbt releasePublishLocal in it (JDK 25), then depend on the version in its version.sbt. sbt resolves the local Ivy repository by default, before the registry, so remove that local copy before you depend on a registry release with the same version. This path has not been exercised by hand; the repository's own consumer check publishes to a temporary Maven directory instead.
  • Reference the h34s source projects from your sbt build.

Pick the modules for your runtime. Kyo applications never need ZIO; ZIO applications get Kyo through the ZIO adapter:

You wantKyoZIO
Handlerskyo-h3-extzio-h3-ext
Schemas (optional)native-converter-kyo-schema-extnative-converter-zio-schema-ext + zio-schema-derivation 1.7.5
Nitro storage (optional)kyo-nuxthub-extzio-nuxthub-ext

On the npm side, every application needs @yuri4n/h34s-nuxt (a dev dependency) and h3@1.15.4 (a runtime dependency). See npm tooling.

Toolchain

ToolVersionWhy
JDK25Runs sbt; Kyo 1.0.0-RC7 macros need it. Not needed in production
sbt1.11.7Builds and links Scala.js
Scala / Scala.js3.9.0 / 1.22.0Every h34s module uses this pair
Node / pnpm24 / 10.34.4Runs Nuxt and your handlers

See compatibility for what was tested.

1. Start a new project

The project tour arranges the demo's greeting files in layout A: the setup files from this guide, and the demo's greeting handler, models and AppHandler as the Scala code. It is not a generated or compiled project. The Scala files also use the schema bridge (native-converter-kyo-schema-ext, or native-converter-zio-schema-ext with zio-schema-derivation), and the ZIO AppHandler also wires the demo's Notes service. Pick a file to read it, and switch between Kyo and ZIO:

A new project

This tour arranges the demo's greeting files in layout A; it is not a generated or compiled project, because its Scala files also use the schema bridge (native-converter-kyo-schema-ext, or native-converter-zio-schema-ext with zio-schema-derivation) and the ZIO AppHandler also wires the demo's Notes service.

  • project/
  • scala-server/src/main/scala/
  • server/.scala/ generated by the link step
  • app/
scala-server/src/main/scala/Handlers.scalaFrom examples/nuxt/scala/kyo/src/main/scala/dev/yuri4n/kyoexample/Handlers.scala
package dev.yuri4n.kyoexample

import dev.yuri4n.exts.kyoh3.*
import kyo.*

import scala.scalajs.js.annotation.JSExportTopLevel

@JSExportTopLevel("default", "kyo-greeting-get")
val greeting: EventHandler = AppHandler
  .define: _ =>
      Env.use[Greetings](_.greet("Kyo"))
  .at(Route.get("/api/kyo/greeting"))

@JSExportTopLevel("default", "kyo-greeting-post")
val personalizedGreeting: EventHandler = AppHandler
  .define: request =>
      for
        input    <- request.body.json[GreetingRequest]
        greeting <- Env.use[Greetings](_.greet(input.name))
      yield greeting
  .at(Route.post("/api/kyo/greeting"))

Lines 1, 3, 4, 8, 9, 10, 11, 12, 13, 14, 19 and 21 differ from the ZIO version. A bar at the left edge marks them.

  1. Create a Nuxt 4 application with Nuxt's own tooling. h34s has no project generator.
  2. Add an sbt build at the Nuxt root: build.sbt, project/ and .jvmopts next to nuxt.config.ts, and your Scala sources in scala-server/. See Configure the Scala.js module.
  3. Add the npm package and the Nuxt module, as in npm tooling.
  4. Write, mount and run a handler.

To see a working setup first, follow the getting-started tutorial, or run the demo in examples/nuxt/ with the development guide. Its Kyo greeting needs no database. The demo is a reference, not a template: you do not need to clone this repository to use h34s.

2. Add Scala handlers to an existing Nuxt app

Keep your TypeScript handlers. Scala handlers live next to them and declare their own routes.

  1. Add an sbt build at the Nuxt root, with your Scala sources in scala-server/, as in Configure the Scala.js module. Point its linker output at a generated directory such as server/.scala. If you keep a separate sbt build next to the app instead, set sbtRoot in h34s.config.mjs.
  2. Add @yuri4n/h34s-nuxt and h3@1.15.4, and register the module in nuxt.config.ts. See npm tooling.
  3. Move only the routes you want to Scala. A TypeScript route and a Scala route can share a path prefix. Give each Scala route its own path and method: two handlers for the same route and method conflict.
  4. Read your existing runtimeConfig keys from Scala with RuntimeConfig (Kyo) or ZRuntimeConfig (ZIO). See configuration.

The h34s wire format wraps values in {"data": ...}. If existing clients expect the raw value, pass HandlerOptions(success = SuccessEnvelope.Raw) for those routes. Raw sends a string as text/plain; charset=utf-8 and any other value as JSON (application/json), unless you set a content type.

3. Add Nuxt to an existing Scala.js project

Keep your sbt build where it is, and put the Nuxt application in web/ below it. Your build.sbt does not change: the h34s settings go in a new h34s.sbt next to it, and sbt loads both files.

Status. sbtRoot is new in @yuri4n/h34s-nuxt 0.3.0-alpha.1, which is not released yet. With 0.2.0-alpha.1, create the Nuxt application at the root of your sbt build instead, as in path 1, and keep the h34s settings in h34s.sbt with the linker output server/.scala.

  1. Create a Nuxt 4 application in web/ with Nuxt's own tooling.
  2. Check project/. If no file there adds sbt-scalajs 1.22.0 or later, add or update the line from Configure the Scala.js module. If .jvmopts is missing, add it from the same section.
  3. Create h34s.sbt next to build.sbt, with a dedicated server module that links into web/server/.scala:
    // h34s.sbt
    import org.scalajs.linker.interface.{ModuleKind, OutputPatterns}
    
    lazy val server = project.in(file("scala-server"))
      .enablePlugins(ScalaJSPlugin)
      .settings(
        scalaVersion := "3.9.0",
        libraryDependencies += "dev.yuri4n" %%% "kyo-h3-ext" % "0.2.0-alpha.1",
        scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)
          .withOutputPatterns(OutputPatterns.fromJSFile("%s.js"))),
        Compile / fastLinkJS / scalaJSLinkerConfig ~= (_.withSourceMap(true)),
        Compile / fastLinkJS / scalaJSLinkerOutputDirectory :=
          (LocalRootProject / baseDirectory).value / "web" / "server" / ".scala",
        Compile / fullLinkJS / scalaJSLinkerOutputDirectory :=
          (LocalRootProject / baseDirectory).value / "web" / "server" / ".scala",
      )
    

    Keep your models and services in their own modules. To use one from the server module, add .dependsOn(LocalProject("core")), with the ID of your project instead of core. If your build already has a project named server, pick another name and use it in h34s.config.mjs.
  4. In web/, add the npm package, the Nuxt module and the scripts, as in npm tooling.
  5. Create web/h34s.config.mjs with sbtRoot: '..', so that h34s-scala runs sbt in your build:
    import { defineConfig } from '@yuri4n/h34s-nuxt/build'
    
    export default defineConfig({
      sbtRoot: '..',
      modules: [{ project: 'server', output: 'server/.scala' }],
    })
    
  6. Write, mount and run a handler. Put the handler in scala-server/src/main/scala/, skip the step that writes h34s.config.mjs, and run the scripts in web/.

Only Scala.js-compatible code can enter Nitro. Your shared models must cross-build for Scala.js. Use Scala 3.9.0 for the modules that h34s handlers depend on.

If your backend is JVM-only, you have two choices: cross-build the models and services that can run on Scala.js, or keep the JVM server and call it over HTTP from Nuxt. JDBC libraries, including Doobie, do not run in Scala.js. For PostgreSQL inside Nitro, use kyo-sql or Skunk; see databases.

Configure the Scala.js module

# project/build.properties
sbt.version=1.11.7
// project/plugins.sbt
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0")

Create .jvmopts next to build.sbt, with the JVM options for sbt:

-Xss4m
-Xmx4g

The repository's own builds use these options, and its fresh-consumer check (scripts/check-consumers.mjs) copies the same file into each consumer project. A build without them has not been tested.

// build.sbt
import org.scalajs.linker.interface.{ModuleKind, OutputPatterns}

lazy val server = project.in(file("scala-server"))
  .enablePlugins(ScalaJSPlugin)
  .settings(
    scalaVersion := "3.9.0",
    libraryDependencies += "dev.yuri4n" %%% "kyo-h3-ext" % "0.2.0-alpha.1",
    scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule)
      .withOutputPatterns(OutputPatterns.fromJSFile("%s.js"))),
    Compile / fastLinkJS / scalaJSLinkerConfig ~= (_.withSourceMap(true)),
    Compile / fastLinkJS / scalaJSLinkerOutputDirectory :=
      (LocalRootProject / baseDirectory).value / "server" / ".scala",
    Compile / fullLinkJS / scalaJSLinkerOutputDirectory :=
      (LocalRootProject / baseDirectory).value / "server" / ".scala",
  )

For ZIO, replace kyo-h3-ext with zio-h3-ext. Run sbt with JDK 25.

OutputPatterns.fromJSFile("%s.js") makes each exported module a <name>.js file, which Nitro mounts. Keep only generated files in server/.scala: a production build clears it.

Write, mount and run a handler

  1. Write a handler, export it as the default export of its own module, and give it a route with .at:
    import dev.yuri4n.exts.kyoh3.*
    import org.getshaka.nativeconverter.NativeConverter
    
    import scala.scalajs.js.annotation.JSExportTopLevel
    
    final case class Greeting(message: String) derives NativeConverter
    
    @JSExportTopLevel("default", "hello-get")
    val hello: EventHandler = KHandler.define(_ => Greeting("Hello from Scala")).at(Route.get("/api/hello"))
    
  2. Point h34s.config.mjs at the linked output:
    import { defineConfig } from '@yuri4n/h34s-nuxt/build'
    
    export default defineConfig({
      modules: [{ project: 'server', output: 'server/.scala' }],
    })
    
  3. Link and start Nuxt with the scripts from npm tooling: pnpm dev:scala. The link step prints [h34s] 1 route and 0 models in server/.scala.
  4. Request http://127.0.0.1:3000/api/hello. Expected body: {"data":{"message":"Hello from Scala"}}.

You write no route file: the Nuxt module mounts the route from the manifest that h34s-scala writes after linking, and $fetch('/api/hello') is typed from the generated declaration. derives NativeConverter types the body as unknown; derive NativeSchema from a schema to get its fields. See routes and typed $fetch.

If the request returns 404, check that the link step printed the route; if Nuxt warns that server/.scala/h34s-routes.json is missing, link first and restart Nuxt. If Nitro cannot resolve h3, add h3@1.15.4 to your app's dependencies.

Grow from there

What is verified

The repository's consumer check builds four fresh sbt projects (Kyo, ZIO, Kyo Schema, ZIO Schema) against a temporary Maven repository and serves real h3 requests. A complete Nuxt application created from scratch with these steps has not been verified yet, on any path.

For path 3, the unit tests of @yuri4n/h34s-nuxt run the CLI with sbtRoot and a fake sbt. Once, in a cloud session on 2026-10-20, h34s-scala dev in web/ linked a real sbt build of this shape: an unchanged build.sbt, a new h34s.sbt whose server module depends on a project of build.sbt through LocalProject, and the output in web/server/.scala. That run used no h34s library and no Nuxt.