Getting started
h34s lets you write Nuxt server routes in Scala.js. Kyo is the core: one runtime owns the request lifecycle, cancellation, streaming and errors. A first-class ZIO adapter sits on top of it for ZIO applications.
In this tutorial you run the repository's demo, call a Kyo handler, trigger a validation error, and look at the code behind it. Then you are ready to add h34s to your own app.
Status. This site describes the published
0.2.0-alpha.1. The older0.1.0-alpha.1has a different API; the migration guide lists every change.
Prerequisites
| 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 |
| Node.js | 24 | Runs Nuxt and your handlers |
| pnpm | 10.34.4 | Installs the demo |
Scala 3.9.0 and Scala.js 1.22.0 come from the build. You need no database for this tutorial.
Link and start the demo
Clone yuri4n/h34s and check out the v0.2.0-alpha.1 tag. From the repository root, with JDK 25 selected:
pnpm install --frozen-lockfile
pnpm run scala:fast
pnpm dev
scala:fast links the demo's Kyo and ZIO handlers into examples/nuxt/server/.scala-kyo and examples/nuxt/server/.scala. Wait until Nuxt prints its address.
Make your first request
In another terminal:
curl http://127.0.0.1:3000/api/kyo/greeting
Expected body:
{ "data": { "message": "Hello, Kyo!" } }
Every value body is wrapped in data. Now send a name:
curl -H 'content-type: application/json' --data '{"name":"Scala"}' \
http://127.0.0.1:3000/api/kyo/greeting
Expected body: {"data":{"message":"Hello, Scala!"}}.
Trigger a validation error
Send a blank name:
curl -i -H 'content-type: application/json' --data '{"name":" "}' \
http://127.0.0.1:3000/api/kyo/greeting
The status is 422 and the body names the field:
{ "errors": [{ "message": "must not be blank", "path": "name" }] }
Send {not json and you get a 400 with Request body is not valid JSON. Send {"name":42} and you get a 422 with one generic entry. No input value ever appears in an error.
Evidence. On macOS x86_64, these responses came back from a production build of the demo, under
nuxt dev, and from the linked modules behind a plain h3 server; see compatibility.
Read the code
The handler is a function from the request to the response body (examples/nuxt/scala/kyo/.../Handlers.scala):
@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
The model carries its validation in one schema, so the handler repeats no checks (Greetings.scala):
final case class GreetingRequest(name: String) derives NativeSchema
object GreetingRequest:
given Schema[GreetingRequest] = Schema[GreetingRequest]
.check(_.name)(_.trim.nonEmpty, "must not be blank")
.checkMaxLength(_.name)(80)
AppHandler is the demo's own wiring point. It provides the Greetings service and sets Cache-Control: no-store on every response:
object AppHandler:
private val options = HandlerOptions(defaults = ResponseHead.noStore)
def define[B: ResponseBody](
program: Request => B < (HandlerEffects & Env[Greetings]),
)(using Frame): EventHandler =
KHandler.define(options)(request => Env.run(Services.greetings)(program(request)))
AppHandler, Greetings and GreetingRequest are application code. h34s exports KHandler, Request, Response, HandlerOptions and the other values you see.
The handler declares its own route, .at(Route.post("/api/kyo/greeting")); the demo has no route files. After linking, the h34s Nuxt module mounts it, and Nuxt types $fetch('/api/kyo/greeting'). See routes and typed $fetch.
Try the ZIO route
The demo also has a ZIO handler that streams three strings:
curl http://127.0.0.1:3000/api/test
The body is abc.
The ZIO greeting takes the same body as the Kyo one and needs no database:
curl -H 'content-type: application/json' --data '{"name":"Scala"}' \
http://127.0.0.1:3000/api/zio/greeting
Expected body: {"data":{"message":"Hello, Scala!"}}. See ZIO handlers for the adapter.
Edit and repeat
Keep continuous linking running in a second terminal:
pnpm run scala:watch
Edit a Scala file, wait for linking and for Nitro to restart its worker, then send the request again. In development, an unexpected failure includes its cause and stack in the response; see debugging.
Run the tests
The Scala suites need two small npm fixtures:
for fixture in scala-modules/*/test-js; do npm ci --prefix "$fixture" --ignore-scripts; done
sbt 'h3Ext/test' 'kyoH3Ext/test' 'nativeConverterKyoSchemaExt/test' 'kyoNuxtHubExt/test' \
'zioH3Ext/test' 'nativeConverterZIOSchemaExt/test' 'zioNuxtHubExt/test' 'nuxtsvr/test'
node --test scripts/*.test.mjs packages/nuxt/test/*.test.mjs
None of them needs PostgreSQL or a running Nuxt server.
Next steps
- Adopt h34s in your project: new app, existing Nuxt app, or existing Scala.js project.
- Kyo handlers: requests, responses, errors and services.
- Configuration and lifetimes for real applications.