Adoption paths
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:
- Start a new project
- Add Scala handlers to an existing Nuxt app
- 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 releasePublishLocalin it (JDK 25), then depend on the version in itsversion.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 want | Kyo | ZIO |
|---|---|---|
| Handlers | kyo-h3-ext | zio-h3-ext |
| Schemas (optional) | native-converter-kyo-schema-ext | native-converter-zio-schema-ext + zio-schema-derivation 1.7.5 |
| Nitro storage (optional) | kyo-nuxthub-ext | zio-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
| Tool | Version | Why |
|---|---|---|
| JDK | 25 | Runs sbt; Kyo 1.0.0-RC7 macros need it. Not needed in production |
| sbt | 1.11.7 | Builds and links Scala.js |
| Scala / Scala.js | 3.9.0 / 1.22.0 | Every h34s module uses this pair |
| Node / pnpm | 24 / 10.34.4 | Runs 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/
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.
- Create a Nuxt 4 application with Nuxt's own tooling. h34s has no project generator.
- Add an sbt build at the Nuxt root:
build.sbt,project/and.jvmoptsnext tonuxt.config.ts, and your Scala sources inscala-server/. See Configure the Scala.js module. - Add the npm package and the Nuxt module, as in npm tooling.
- 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.
- 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 asserver/.scala. If you keep a separate sbt build next to the app instead, setsbtRootinh34s.config.mjs. - Add
@yuri4n/h34s-nuxtandh3@1.15.4, and register the module innuxt.config.ts. See npm tooling. - 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.
- Read your existing
runtimeConfigkeys from Scala withRuntimeConfig(Kyo) orZRuntimeConfig(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.
- Create a Nuxt 4 application in
web/with Nuxt's own tooling. - Check
project/. If no file there addssbt-scalajs1.22.0 or later, add or update the line from Configure the Scala.js module. If.jvmoptsis missing, add it from the same section. - Create
h34s.sbtnext tobuild.sbt, with a dedicated server module that links intoweb/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 ofcore. If your build already has a project namedserver, pick another name and use it inh34s.config.mjs. - In
web/, add the npm package, the Nuxt module and the scripts, as in npm tooling. - Create
web/h34s.config.mjswithsbtRoot: '..', so thath34s-scalaruns sbt in your build:import { defineConfig } from '@yuri4n/h34s-nuxt/build' export default defineConfig({ sbtRoot: '..', modules: [{ project: 'server', output: 'server/.scala' }], }) - Write, mount and run a handler. Put the handler in
scala-server/src/main/scala/, skip the step that writesh34s.config.mjs, and run the scripts inweb/.
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
- Write a handler, export it as the
defaultexport 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")) - Point
h34s.config.mjsat the linked output:import { defineConfig } from '@yuri4n/h34s-nuxt/build' export default defineConfig({ modules: [{ project: 'server', output: 'server/.scala' }], }) - 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. - 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
- Wire your services in one
AppHandler: Kyo or ZIO. - Read configuration and keep secrets in
Secret. - Hold pools in
Shared/ZShared: lifetimes. - Validate input with schemas.
- Add streaming, storage, tests and a database.
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.