# Getting started > Run the demo, make your first request to a Kyo handler, and see validation and errors on the wire. 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](/guide/adoption). > **Status.** This site describes the [published `0.2.0-alpha.1`](https://github.com/yuri4n/h34s/releases/tag/v0.2.0-alpha.1). The older `0.1.0-alpha.1` has a different API; the [migration guide](/reference/migration) 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 |
| 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
|
| 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 |
| Member | Value |
|---|---|
method
,
path
|
Upper-case method; path and query as h3 reports them |
header(name)
,
headers
|
Case-insensitive lookup; duplicate headers joined with
", "
|
headerValues(name)
|
Every value of a header, in request order |
query
,
queryAll
|
First value per key; all values per key |
routerParam(name)
,
routerParams
|
Decoded router parameters |
cookie(name)
,
cookies
|
Parsed cookies |
bearerToken
|
The token of exactly one
Authorization: Bearer
header, else
None
|
development
,
debug
|
Set only by the h34s Nitro plugin; a request cannot set them |
body
|
The one-shot request body |
| Situation | Result |
|---|---|
Content-Type
is not JSON (
application/json
,
application/*+json
), or is absent while the request has a body; with
HandlerOptions(requireJsonContentType = false)
, an absent one is accepted
|
HttpError
415
|
Body larger than the limit (checked from
Content-Length
, then while reading)
|
HttpError
413; h34s never buffers more than the limit. A body without a content type is read up to the limit before the 415, so one above the limit answers 413
|
| Text is not valid JSON |
HttpError
400
|
| JSON does not match the converter's shape |
InvalidInput
422 with a generic message
|
| A schema-backed converter rejects the value |
InvalidInput
422 with your schema's messages; see
schemas
|
| Any other exception from the converter | A defect: opaque 500 |
| Return type | Response |
|---|---|
Any
A
with a
NativeConverter
|
200 (or the status you set) with
{"data":
|
Unit
|
204 with no body, unless you set another status |
Sent
, from
Response.sendStream(stream)
|
The stream's strings, written as UTF-8 text as they come; see streaming |
| Operation | Effect |
|---|---|
Response.status(status)
|
Last write wins. Use
Status.Created
or
Status.unsafe(code)
for literals
|
Response.header(name, value)
|
Replaces earlier values of that header |
Response.appendHeader(name, value)
|
Adds another value |
Response.cookie(SetCookie(...))
|
Set-Cookie
. Defaults:
Path=/
,
HttpOnly
,
Secure
,
SameSite=Lax
|
Response.deleteCookie(name, path, domain, secure, onFailure)
|
Expires a cookie.
path
(default
/
) and
domain
must match the cookie;
Secure
is sent by default, which browsers need to delete a
__Host-
or
__Secure-
cookie
|
Response.redirect(location, status)
|
A 3xx status (default
Status.Found
) and
Location
|
Response.head
|
What your program has set so far, without the handler defaults |
Response.sendStream(stream)
|
Writes the head now, then the stream, and gives
Sent
; make it the last step. See
streaming
|
Response.sendEvents(events)
|
sendStream
for server-sent events: sets the event-stream headers, then writes one frame per
ServerEvent
. See
server-sent events
|
| Failure | Status | Body (default options) |
|---|---|---|
Abort.fail(safeError)
|
safeError.status
(400 by default)
|
{"errors":[{"message":"..."}]}
|
KHandler.failWith(status, message)
(
Nothing < Abort[HttpError]
)
|
status
|
the same shape |
InvalidInput
from a body read
|
422 |
one entry per schema message, with
path
when known, never input values
|
Anything else: thrown exceptions, panics, a
SafeError
that is thrown
|
500 |
{"errors":[{"message":"Internal server error"}]}
|
| Option | Default | Alternatives |
|---|---|---|
success
|
SuccessEnvelope.Data
:
{"data": value}
|
SuccessEnvelope.Raw
: the converted value without the envelope. A string is sent as
text/plain; charset=utf-8
and any other value as JSON (
application/json
), unless you set a content type;
X-Content-Type-Options: nosniff
comes with the default. A
null
or
undefined
value becomes 204
|
errors
|
ErrorEnvelope.List
: always
{"errors":[...]}
|
ErrorEnvelope.SingleOrList
:
{"error":{...}}
for one entry,
{"errors":[...]}
for several
|
defaults
|
ResponseHead.empty
|
For example
ResponseHead.noStore
:
Cache-Control: no-store
on success and failure
|
bodyLimit
|
BodyLimit.default
(1 MiB)
|
Any
BodyLimit(bytes)
|
requireJsonContentType
|
true
:
json[A]
answers 415 for a body without a content type
|
false
: accept such a body as JSON; see
cross-site requests
|
| Call | Type |
|---|---|
request.body.json[A]
|
IO[HttpError | InvalidInput, A]
|
request.body.json[A](limit)
|
the same, with an explicit
BodyLimit
|
request.body.text()
|
IO[HttpError, String]
|
request.param(name)
|
IO[HttpError, String]
; 400 when absent
|
| Return type | Response |
|---|---|
Any
A
with a
NativeConverter
|
200 (or the status you set) with
{"data":
|
Unit
|
204 with no body, unless you set another status |
Sent
, from
Response.sendStream(stream)
|
The stream's strings, written as UTF-8 text as they come; see streaming |
| Failure | Status | Body (default options) |
|---|---|---|
ZIO.fail(safeError)
|
safeError.status
|
{"errors":[{"message":"..."}]}
|
ZHandler.failWith(status, msg, msgs*)
|
status
|
one entry per message |
InvalidInput
from a body read
|
422 |
one entry per schema message, with
path
when known
|
| Any other typed failure, a defect, interruption, or several failures | 500 |
{"errors":[{"message":"Internal server error"}]}
|
| Alias | Meaning |
|---|---|
HandlerEnv
|
ZState[ResponseHead] & Scope & HandlerTransport & Fetch
|
ZHandler[R, E, A]
|
ZIO[HandlerEnv & R, E, A]
|
Handler[E, A]
,
TaskHandler[A]
,
URHandler[R, A]
,
UHandler[A]
,
RHandler[R, A]
|
The usual ZIO shapes over
HandlerEnv
|
TaskStream[A]
,
RStream[R, A]
,
URStream[R, A]
|
ZStream
shapes
|
| You need | Kyo | ZIO |
|---|---|---|
| The config for this request |
RuntimeConfig.get[AppConfig](request)
|
ZRuntimeConfig.get[AppConfig](request)
|
| The startup config, for application resources |
RuntimeConfig.load[AppConfig]
|
ZRuntimeConfig.load[AppConfig]
|
| Key | Variable |
|---|---|
database.host
|
NUXT_DATABASE_HOST
|
database.port
|
NUXT_DATABASE_PORT
|
database.password
|
NUXT_DATABASE_PASSWORD
|
| Situation | Message |
|---|---|
| The h34s Nitro plugin is not installed (the module is missing) |
h34s Nitro plugin is not installed
|
| A field has the wrong type |
Nuxt runtime config does not match the expected shape at root.database.port
|
| Reading the config throws |
h34s could not read the Nuxt runtime config
|
| Lifetime | Use it for | Kyo | ZIO |
|---|---|---|---|
| One request | A pooled connection, a transaction, a temporary file, a stream's source |
The request
Scope
in
HandlerEffects
|
The request
Scope
in
HandlerEnv
|
| The whole application | A connection pool, an HTTP client, a cache handle |
Shared
|
ZShared
|
| Kyo | ZIO | |
|---|---|---|
| Module |
native-converter-kyo-schema-ext
|
native-converter-zio-schema-ext
|
| Package |
dev.yuri4n.exts.ntcvkyoschema
|
dev.yuri4n.exts.ntcvzioschema
|
| Derive |
derives Schema, NativeSchema
|
derives Schema, NativeSchema
(needs
import zio.schema.derived
)
|
| JSON |
kyo-schema-json:
Json.encode
,
Json.decode
|
zio-schema-json:
JsonCodec
|
| YAML |
kyo-schema-yaml:
encodeString[Yaml]
,
decodeString[Yaml]
|
Not on Scala.js: zio-json-yaml is published for the JVM only |
| Validation |
Schema[A].check(...)
and friends
|
Validation
in the schema (
.validation
,
@validate
)
|
| Messages | Your check messages | Built from the rule and its bounds |
| Scala | Native value |
|---|---|
None
|
The key is absent. On input, absent,
null
and
undefined
all decode to
None
|
| Enum case, sealed trait case |
A single-key object:
{"Red":{}}
,
{"Circle":{"radius":1.5}}
.
@discriminator("type")
gives
{"type":"Joined",...}
|
Map[String, A]
|
An object |
Map[Int, A]
and other keys
|
An array of
{"key":...,"value":...}
|
BigDecimal
,
BigInt
|
Strings. Decoding rejects (generic 422) a string longer than 1000 characters and a
BigDecimal
exponent beyond +/-10000, before parsing: Scala.js parses big numbers in quadratic time
|
Instant
,
Duration
|
ISO strings |
Bytes (
Span[Byte]
)
|
A base64 string |
Double
NaN and infinities
|
The strings
"NaN"
,
"Infinity"
,
"-Infinity"
|
Long
|
A number; decoding accepts only safe integers (up to 2^53 - 1) |
| Schema form | Native value |
|---|---|
String
,
Boolean
|
Strict primitives |
Int
|
An integer in the 32-bit range |
Double
|
A finite number |
| Records (case classes) |
Plain objects; field names, aliases, defaults, transient fields and
rejectExtraFields
are respected
|
Option
|
Absent or
null
is
None
|
| Sequences | Arrays |
| Lazy and transformed schemas | Through their underlying schema |
| Bridge | TypeScript type |
|---|---|
| Kyo |
The shapes in the table above: records with their wire names (
@rename
), optional fields as
name?: T | null
, collections as arrays,
Map[String, A]
as
Record
, other maps as
{ key; value }
arrays, sealed traits and enums as one-key objects or
@discriminator
objects,
Double
and
Float
as
number | "NaN" | "Infinity" | "-Infinity"
, big numbers, instants, durations and bytes as strings
|
| ZIO |
Records with their wire names (
@fieldName
) and without
@transientField
fields,
Option
as
T | null
, sequences as arrays,
String
,
Boolean
,
Int
,
Double
; a transform has its underlying type; a recursive record refers to its model by name where it repeats (
unknown
when the type is written out)
|
| Problem | Status | Entries |
|---|---|---|
| Body is not valid JSON | 400 |
Request body is not valid JSON
|
| Wrong shape | 422 | One generic entry, no input values |
| Failed checks | 422 |
One entry per check, with
path
when known
|
| Exception in a transform, validator or converter | 500 | Opaque; diagnostics only in development or debug mode |
| Module | Schema dependencies |
|---|---|
native-converter-kyo-schema-ext
|
kyo-schema 1.0.0-RC7. Add kyo-schema-json and kyo-schema-yaml yourself for text formats |
native-converter-zio-schema-ext
|
zio-schema 1.7.5. Add zio-schema-derivation 1.7.5 for
derives Schema
, and zio-schema-json for JSON text
|
kyo-h3-ext
,
zio-h3-ext
|
None. The handler modules never bring a schema library |
| When it fails | Result |
|---|---|
Before
sendStream
|
The normal
failure rule
: an approved
SafeError
response, or an opaque 500
|
| Writing the head, at the call |
For example an invalid cookie domain. Nothing reached the client: h34s clears the head, the stream never starts,
sendStream
fails, and the failure rule answers with an opaque 500. If part of the head had already left, h34s closes the connection instead
|
| The stream, before or after its first element |
The head is already out. h34s closes the connection without a final chunk, so the client sees an incomplete transfer; the elements already written stay.
sendStream
fails with the stream's error, your program sees it, and the server logs it once. Nothing more is written
|
The stream throws, even a
SafeError
, or emits
null
|
A defect of the stream: the same as above. The client never sees the error's text |
| Writing to the response | h34s closes the connection. The log names the write error, not the producer's interruption |
The stream, without a socket (an in-process
$fetch
)
|
The caller's read of the body fails after the elements it already read.
sendStream
fails with the stream's error
|
| Field | Meaning |
|---|---|
data: String
|
The event's text. Each line becomes one
data:
line, and the client joins them with LF, so a CR or CRLF in
data
arrives as LF
|
event: Option[String]
|
The event type.
None
is the default type, which a browser's
EventSource
calls
message
. A named type reaches only
addEventListener(type)
|
id: Option[String]
|
The event's ID. A browser keeps it as
lastEventId
and sends it back in the
Last-Event-ID
header when it reconnects.
Some("")
clears it
|
retry: Option[Int]
|
The time in milliseconds that a browser waits before it reconnects |
| Area | Behavior |
|---|---|
| Direct Node HTTP responses |
Supported. Tested with real h3 and mocked Node responses, and with a real
node:http
server in
NitroLocalFetchSuite
|
No socket (Nitro's in-process local fetch, for example a server-side
$fetch
to your own route)
|
Supported. Tested with a copy of Nitro 2.13.1's local-fetch wiring (
NitroLocal
); not yet tested against
nuxt dev
or a built Nitro server. An unread body that nobody cancels keeps the callee open
|
| Back pressure |
One chunk at a time from your stream. h3's sink does not wait for Node's
drain
, so a slow client can still grow Node's output buffers. There is no end-to-end memory bound
|
| Other Nitro presets (serverless, edge) | Not tested. Many of them serve every request without a socket, so no disconnect reaches h34s unless the platform cancels the body |
| Server-sent events |
No heartbeat comments yet: a proxy with an idle timeout can close a quiet stream. No typed decoder for NDJSON in
$fetch
. In-process, stopping before the end of a JavaScript
createEventStream
route makes h3 leak unhandled rejections
|
| Member | Value |
|---|---|
head
|
The
ResponseHead
that your program set; after
sendStream
, the one at the call
|
status
|
head.status
;
None
when your program set none
|
header(name)
|
The header's values after your sets and appends, joined with
", "
; names ignore case
|
headerValues(name)
|
The same values as a list |
cookies
|
The cookie entries that your program set |
body
|
TestResponse.Body.Value(value)
, or
TestResponse.Body.Streamed(elements)
|
| Module | API | Depends on |
|---|---|---|
kyo-nuxthub-ext
|
KHubKv
(package
dev.yuri4n.exts.kyonuxthub
)
|
the Kyo core |
zio-nuxthub-ext
|
ZHubKv
(package
dev.yuri4n.exts.zionuxthub
)
|
kyo-nuxthub-ext
and the ZIO adapter
|
| Flag | Set by | When |
|---|---|---|
development
|
The plugin, from Nitro's
import.meta.dev
|
nuxt dev
. A production build compiles it to
false
|
debug
|
The plugin, from
runtimeConfig.h34s.debug
, read once at startup
|
NUXT_H34S_DEBUG=true
at runtime, or
runtimeConfig: { h34s: { debug: true } }
in
nuxt.config.ts
|
| Record entry | Used by |
|---|---|
development
(Nitro's
import.meta.dev
)
|
Development diagnostics |
debug
(
runtimeConfig.h34s.debug
, read once at startup)
|
Debug diagnostics; see debugging |
runtimeConfig
(per request and at startup)
|
RuntimeConfig
/
ZRuntimeConfig
; see
configuration
|
storage(base)
(Nitro
useStorage
)
|
KHubKv.mount
/
ZHubKv.mount
; see
Nitro storage
|
onClose
(Nitro's
close
hook)
|
Shared
/
ZShared
release; see
lifetimes
|
| Value | Prerender bundle |
Server output (
.output/server
)
|
|---|---|---|
'bundle'
(the default)
|
The linked Scala.js | The linked Scala.js |
'stub'
|
One stub that answers 501 | The linked Scala.js |
| Files in the directory | Path |
What
init
writes
|
|---|---|---|
nuxt.config.*
, or a
package.json
that lists
nuxt
|
An existing Nuxt app |
The sbt build at the Nuxt root:
build.sbt
, or
h34s.sbt
next to an existing
build.sbt
. It merges the scripts and dependencies into
package.json
and adds the module.
|
build.sbt
|
An existing sbt build |
A new Nuxt app in
web/
with
sbtRoot: '..'
, and
h34s.sbt
next to your
build.sbt
, which
init
does not edit.
|
| Nothing yet | A new project | A minimal Nuxt app, pinned to Nuxt 4.3.1, and its sbt build. |
| Other files | None |
Nothing.
init
stops and asks you to run it in your Nuxt app or sbt build, or with a new directory.
|
| Layer | sbt project / artifact | What it gives you | Depends on |
|---|---|---|---|
| Native layer |
h3Ext
/
h3-ext
|
Pure 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 core |
kyoH3Ext
/
kyo-h3-ext
|
KHandler.define
, the request lifecycle, body reading with limits,
Response
, error rendering, diagnostics,
RuntimeConfig
,
Shared
,
Response.sendStream
and
sendEvents
|
h3-ext
, kyo-core 1.0.0-RC7
|
| ZIO adapter |
zioH3Ext
/
zio-h3-ext
|
ZHandler.define
,
Response
over
ZState
with
sendStream
and
sendEvents
,
ZRuntimeConfig
,
ZShared
, and the awaiting ZIO-to-Kyo bridge
|
kyo-h3-ext
, kyo-zio, ZIO 2.1.26, zio-prelude, zio-json,
native-converter-zio-ext
|
| Kyo Schema bridge |
nativeConverterKyoSchemaExt
/
native-converter-kyo-schema-ext
|
derives Schema, NativeSchema
for Kyo Schema
|
h3-ext
, kyo-schema
|
| ZIO Schema bridge |
nativeConverterZIOSchemaExt
/
native-converter-zio-schema-ext
|
derives Schema, NativeSchema
for ZIO Schema
|
h3-ext
, zio-schema 1.7.5
|
| ZIO converters |
nativeConverterZIOExt
,
nativeConverterZIOJsonExt
|
NonEmptyList
and zio-json
Json
converters,
eimap
|
h3-ext
, ZIO
|
| Storage |
kyoNuxtHubExt
/
kyo-nuxthub-ext
,
zioNuxtHubExt
/
zio-nuxthub-ext
|
KHubKv.mount
,
ZHubKv.mount
over Nitro storage
|
Kyo core; the ZIO one also needs the ZIO adapter |
| npm tooling |
@yuri4n/h34s-nuxt
|
Build CLI, Nuxt module, development loading, source maps, and the Nitro plugin that supplies flags, runtime config, storage and the close hook | Node, Nuxt. No Scala dependency. |
| Your application | your sbt project and Nuxt app |
Models, services,
AppHandler
, resources, route mounts, database and deployment
|
the modules you choose |
| Layer | What lives there | Who owns it |
|---|---|---|
| Business logic |
Your services and programs, as Kyo
A < S
or ZIO effects
|
You |
| Interfaces |
Your service traits (
Greetings
,
Notes
) and the h34s values (
Request
,
SafeError
,
Secret
)
|
You and h34s |
| Implementation |
The h34s runtime, its internal facades, and your service interpreters (
NotesLive
)
|
h34s and you |
| Application |
Your
AppHandler
: configuration, then
Shared
/
ZShared
resources, then
Env
/
ZLayer
wiring
|
You |
| Path | What to read |
|---|---|
scala-modules/h3
|
The native layer and its tests |
scala-modules/kyo-h3
|
The Kyo core and its tests |
scala-modules/zio-h3
|
The ZIO adapter and its tests |
scala-modules/native-converter-kyo-schema
,
native-converter-zio-schema
|
Schema bridges |
scala-modules/native-converter-zio
,
native-converter-zio-json
|
ZIO converters |
scala-modules/kyo-nuxthub
,
zio-nuxthub
|
Storage |
packages/nuxt
|
CLI, Nuxt module, Nitro plugin |
examples/nuxt/scala/kyo
,
examples/nuxt/scala/zio
|
The demo's Kyo and ZIO applications |
docs/
|
The guides and this documentation site |
| Capability | Kyo | ZIO |
|---|---|---|
Handlers as
Request => body
functions
|
KHandler.define
|
ZHandler.define
|
| Response head as local state, written once |
Var[ResponseHead]
|
ZState[ResponseHead]
|
Wire format
{data}
/
{errors:[{message,path}]}
/ 204, configurable
|
HandlerOptions
|
HandlerOptions
|
| Bounded body reads with 400/413/415/422 |
request.body.json
,
text
|
same |
| Safe errors, opaque 500s, development and debug diagnostics | yes |
yes, with
Cause.prettyPrint
|
Runtime config from Scala,
Secret
|
RuntimeConfig
|
ZRuntimeConfig
|
| Application-scoped resources released on Nitro close |
Shared
|
ZShared
|
| Streams with cancellation that waits for cleanup |
kyo.Stream
|
ZStream
|
| Nitro storage |
KHubKv.mount
|
ZHubKv.mount
|
| One schema for native values, JSON, YAML and validation |
Kyo
NativeSchema
|
ZIO
NativeSchema
(no YAML on Scala.js)
|
| Testing without h3 |
Request.fake
+
Var.runTuple
|
Request.fake
+
ZIO.stateful
|
| Artifact | Purpose | Brings |
|---|---|---|
h3-ext
|
Native layer: request snapshot, response head, safe errors,
Secret
, internal h3/Nitro facades
|
No effect runtime |
kyo-h3-ext
|
The core:
KHandler
, body reading,
RuntimeConfig
,
Shared
, streaming
|
Kyo |
kyo-nuxthub-ext
|
Optional Kyo storage over Nitro mounts (
KHubKv.mount
)
|
Kyo |
native-converter-kyo-schema-ext
|
Optional
derives NativeSchema
from a Kyo
Schema
|
Kyo Schema |
zio-h3-ext
|
The ZIO adapter:
ZHandler
,
ZRuntimeConfig
,
ZShared
,
ZStream
bodies
|
ZIO and Kyo |
zio-nuxthub-ext
|
Optional ZIO storage over Nitro mounts (
ZHubKv.mount
)
|
ZIO and Kyo |
native-converter-zio-ext
|
NativeConverter support for ZIO data types | ZIO, no Kyo |
native-converter-zio-json-ext
|
NativeConverter support for ZIO JSON values | ZIO JSON, no Kyo |
native-converter-zio-schema-ext
|
Optional
derives NativeSchema
from a ZIO
Schema
|
ZIO Schema, no Kyo |
| Component | Version |
|---|---|
| Artifacts |
nine
dev.yuri4n
modules,
0.2.0-alpha.1
, GitHub Packages (public)
|
| Scala (every module) | 3.9.0 |
| Scala.js linker | 1.22.0 |
| Build JDK | 25 (checked with Temurin 25.0.4.1+1) |
| sbt | 1.11.7 |
| Kyo | 1.0.0-RC7 (kyo-core, kyo-zio, kyo-schema; kyo-schema-json and kyo-schema-yaml in tests) |
| ZIO | 2.1.26; zio-prelude 1.0.0-RC41; zio-json 0.7.44; zio-schema 1.7.5 |
| NativeConverter | 0.9.0 |
| h3 | 1.15.4 (tests and the demo's runtime dependency) |
| Skunk (demo only) | 1.0.0-M12 |
| Node / pnpm | 24 (checked with 24.19.0) / 10.34.4 |
| Nuxt (demo lockfile) | 4.3.1, with Nitro 2.13.1 |
| npm tooling |
@yuri4n/h34s-nuxt@0.2.0-alpha.1
, private GitHub Packages (the repository demo uses the workspace link)
|
| Check | Result |
|---|---|
h3Ext/test
|
36 passed |
kyoH3Ext/test
|
79 passed |
kyoNuxtHubExt/test
|
15 passed |
nativeConverterKyoSchemaExt/test
|
25 passed |
zioH3Ext/test
|
81 passed |
zioNuxtHubExt/test
|
14 passed |
nativeConverterZIOSchemaExt/test
|
20 passed |
nuxtsvr/test
(ZIO demo)
|
6 passed |
nativeConverterZIOJsonExt
,
zioMigrationsExt
|
compile |
kyoExample/fastLinkJS
,
nuxtsvr/fastLinkJS
|
link |
releaseCheck
(POMs for the 9 release modules)
|
passed |
Node tests:
scripts/*.test.mjs
,
packages/nuxt/test/*.test.mjs
|
All 47 passed (after the Phase D review fixes) |
| Documentation snippets that do not come from compiled sources (Kyo and ZIO guides, configuration, lifetimes, storage, streaming, testing, adoption, and the kyo-sql code with kyo-sql-postgres 1.0.0-RC7) |
Compiled in a temporary test file against this branch (
kyoNuxtHubExt/Test/compile
,
zioNuxtHubExt/Test/compile
); not run
|
Fresh Maven consumers (
scripts/check-consumers.mjs
) for Kyo, ZIO, Kyo Schema and ZIO Schema, from a temporary Maven repository
|
Compiled, linked and served real h3 requests with the new wire format; the Kyo consumers resolved no
dev.zio
artifact
|
| Kyo demo over a plain h3 server with a preemptive router (Nitro's router mode) |
GET and POST 200 with
{"data":...}
; 422 entries with
path
for a blank or 81-character name; generic 422 for a wrong shape; 400 for invalid JSON;
Cache-Control: no-store
on every response
|
| ZIO demo over the same h3 server |
/api/test
answers
abc
;
/api/notes
without the plugin is an opaque 500; with a fake plugin record and no database, a 500 with
debug.cause
and no password in it, and one close hook registered and released
|
Production build of the demo (Scala full link,
nuxt build
, then the Node server from
.output/
)
|
Built with
NODE_OPTIONS=--max-old-space-size=8192
(the default heap ran out of memory). Kyo routes: the same 200, 422, 400 responses, and 415 for
text/plain
.
/api/test
streamed
abc
.
/api/notes
without a database: opaque 500. With
NUXT_H34S_DEBUG=true
:
debug.cause
present, password absent.
SIGTERM
: the
ZShared
pool registered on the first Notes request was released, exit status 0
|
| Production build after the review fixes (same machine, same day) |
kyo-smoke.mjs
passed with the default salutation and with
NUXT_GREETING_SALUTATION=Hi
(Kyo
RuntimeConfig.load
in a
Shared
);
smoke.mjs
with
H34S_SMOKE_SKIP_DATABASE=true
passed without PostgreSQL (invalid Notes input is a 422 before any database work); streams carry
text/plain; charset=utf-8
and
nosniff
by default
|
nuxt dev
on the demo (Nuxt 4.3.1,
pnpm dev
after
pnpm run scala:fast
)
|
Kyo GET 200 with
{"data":...}
and
no-store
; a blank name 422 with path
name
;
/api/test
answered
abc
;
/api/notes
without a database answered a 500 with
debug.cause
(Scala stack frames, no password). A Kyo handler that threw a JavaScript
TypeError
(a temporary probe route) had its stack mapped to
RequestLifecycle.scala
and
KHandler.scala
|
scripts/check-npm-nuxt.mjs
(the packed module in a synthetic Nuxt 4.3.1 project)
|
Development diagnostics, direct loading, Scala source maps, a relink reload, production bundling, and
NUXT_H34S_DEBUG=true
at runtime
|
| Check | Result |
|---|---|
Production build of the demo (
pnpm run scala:full
, then
nuxt build
with
--max-old-space-size=8192
)
|
Passed; 2:14 for
pnpm run scala:full
(cold sbt start, compile and link), 2:15 for the Nuxt build, 6.5 GiB peak RSS
|
ZIO Notes against PostgreSQL 16 with
scram-sha-256
(production server,
.env.example
variables)
|
smoke.mjs
passed with the database.
POST /api/notes
201; blank text 422 with path
text
;
{"text":42}
the generic 422;
GET /api/notes
200 with the note
|
| Skunk 1.0.0-M12 SCRAM login in the production bundle |
PostgreSQL logged
connection authenticated: identity="h34s" method=scram-sha-256
|
SIGTERM
with 4 pooled sessions open
|
Exit status 0; 4 disconnections logged; 0 sessions of the user in
pg_stat_activity
|
kyo-sql code of the
database guide
, kyo-sql-postgres 1.0.0-RC7, in Node through
sbt kyoExample/test
with
H34S_POSTGRES_URL
|
5 passed, twice: insert and list, unique violation to
DuplicateNote
, rollback on a non-SQL
Abort
, rollback (with
SqlRequestTransactionFailedStatementException
) when the body recovers from a failed statement, and a failed nested transaction that rolls back only its own part. The nested test also checks that the nested step failed with SQLSTATE 23505. After each run, no suite row was left in
notes
. kyo-net selected its
node
backend by itself. Without the variable, the 5 tests are skipped; with
H34S_POSTGRES_REQUIRED=true
and no variable, they fail
|
pnpm run test:package
with the
scalaPrerender: 'stub'
checks (commit
ae4c502
)
|
Passed.
check-npm-nuxt.mjs
set the option in the packed consumer. A build that also prerendered a route calling a Scala.js route through
event.$fetch
, and catching the 501, failed with the
[h34s]
warning and the list of requests. The build that prerendered only a route without Scala.js passed: its prerender bundle had the stub and no linked code, its server output had the linked code and no stub, and the production server ran a directly mounted linked handler. Without the
prerender:done
report or without the stub plugin, the check failed
|
pnpm run test:package
after the review fixes (commit
68f6100
)
|
Passed in 44 s. The build that prerendered the route calling a Scala.js route failed with the
[h34s]
warning and the list, and its log had no
[request error]
or
[unhandled]
line. A third build also prerendered the Scala.js route itself: it failed with Nitro's
Errors prerendering:
summary and
Exiting due to prerender errors.
, h34s logged the list of both requests, and the log had no unhandled-error line
|
| Variant | Heap limit | Result | Peak RSS | Wall time |
|---|---|---|---|---|
| Baseline (both applications, server source maps, prerendering) | 8192 MiB (the scripts' option) | Passed | 6.50 GiB | 2:15 |
| Baseline |
Node's real default on this VM (2240 MiB;
NODE_OPTIONS
removed)
|
Out of memory while it built the prerender bundle ("Initializing prerenderer") | 2.58 GiB | 1:00 |
| Baseline | 6144 MiB | Passed | 5.46 GiB | 2:06 |
| Baseline | 4096 MiB | Out of memory in "Building Nuxt Nitro server", after prerendering | 4.66 GiB | 2:36 |
No server source maps (
sourcemap.server=false
in
.nuxtrc
)
|
4096 MiB |
Passed; no
.map
file in
.output/server
|
4.34 GiB | 1:56 |
No prerendering (a temporary
prerender:routes
hook that clears the one route)
|
4096 MiB | Passed | 4.02 GiB | 1:16 |
| Kyo application only (the ZIO route files moved away) | 4096 MiB | Passed; prerendering took 17 s | 3.41 GiB | 1:01 |
| ZIO application only (the Kyo route files moved away) | 4096 MiB | Passed; prerendering took 33 s | 4.46 GiB | 1:46 |
No full locale database (
locales-full-currencies-db
excluded from
nuxtsvr
too)
|
Not built |
The database jar is on the ZIO application's classpath (
show nuxtsvr/Compile/fullClasspath
), but excluding it did not change the linked output (same name, size and time stamp), so the build input does not change
|
Not measured | Not measured |
| Baseline, a second run on the same day | 8192 MiB | Passed; prerendering took 54 s | 6.42 GiB | 2:13 |
Stub through a temporary
nuxt.config
hook that added the same Rollup plugin to the prerender build, before the option existed
|
4096 MiB |
Passed; prerendering took 5.7 s. The plugin replaced exactly the 5 Scala.js route entries (
kyo-greeting-get
,
kyo-greeting-post
,
notes-get
,
notes-post
,
test
). The prerendered
sql_dump.txt
had the baseline's SHA-256 (
15069d1d1d010d84d8b3f879a12c2efaf4335d6e5feb5c154127f0df1574573d
), and the stub's text was not in
.output/server
. The
postgres
CI job's serve step, run on this VM (
smoke.mjs
with PostgreSQL, 4 sessions before
SIGTERM
, exit status 0, 0 after), passed on this build
|
4.13 GiB | 1:41 |
| The same hook |
Node's real default on this VM (2240 MiB;
NODE_OPTIONS
removed)
|
Prerendering passed (3.5 s), then out of memory in "Building Nuxt Nitro server" | 2.58 GiB | 0:43 |
scalaPrerender: 'stub'
, the module option (commit
cca91f5
)
|
4096 MiB |
Passed; prerendering took 3.8 s.
sql_dump.txt
had the same SHA-256. The same serve step passed on this build. The contents of the prerender bundle and of
.output/server
were not recorded for this build
|
4.05 GiB | 1:23 |
scalaPrerender: 'stub'
, the module option with the request report (commit
ae4c502
)
|
4096 MiB |
Passed; prerendering took 3.6 s, and no request reached the stub. The prerender bundle (90 files, 1,207,913 bytes, about 1.2 MB;
du -sh
reports 1.5M of disk use) mounts the stub for all 5 Scala.js routes;
.output/server
bundles the Scala.js and has no stub text.
sql_dump.txt
had the same SHA-256. The same serve step passed, and
kyo-smoke.mjs
passed against the same server build, which then exited with status 0 on
SIGTERM
|
4.07 GiB | 1:18 |
scalaPrerender: 'stub'
, the stub's 501 as a handled h3 error (commit
68f6100
)
|
4096 MiB |
Passed; prerendering took 3.9 s, and no request reached the stub (no
[h34s]
,
[request error]
or
[unhandled]
line). The prerender bundle (90 files, 1,208,871 bytes, about 1.2 MB) mounts the stub for all 5 Scala.js routes and has no Scala.js classes;
.output/server
bundles the Scala.js and has no stub text.
sql_dump.txt
had the same SHA-256. This build was not served
|
4.06 GiB | 1:18 |
| Area | Status |
|---|---|
Relinking the real Scala.js demo under
nuxt dev
, and breakpoint stepping
|
Not checked (the relink reload was checked on a synthetic project) |
The Nitro close hook when
nuxt dev
replaces its worker
|
Not checked;
SIGTERM
in production was
|
kyo-sql / kyo-sql-postgres inside Nitro, including
KYO_NET_BACKEND=node
|
Not run. The database guide service code ran against PostgreSQL 16 in Node through sbt (see above), not in a Nitro bundle |
kyo-sql guide code vs
KyoSqlPostgresSuite
|
Copied by hand; no check keeps them identical (byte-identical on 2026-10-19). The
postgres
CI job compiles and runs the suite, not the guide
|
| PostgreSQL in CI |
The
postgres
job of the Scala.js libraries workflow runs PostgreSQL 16 with SCRAM, the kyo-sql suite (
H34S_POSTGRES_REQUIRED=true
, so it cannot skip), a check that the suite left no rows, the demo's production build,
smoke.mjs
and the
SIGTERM
session check. It runs on pull requests and pushes to
main
that touch the libraries or the example. It skips release tags (
release.yml
reuses this workflow), so it does not gate a release. Its first run on
#45
failed: the
SIGTERM
check counted PostgreSQL's logical replication launcher, which runs as the bootstrap superuser
h34s
in the job's container. The
corrected job
passed on
042cef3
on GitHub's
ubuntu-24.04
runner: the suite 5 of 5 with 0 rows left,
pnpm build
in 1:43,
smoke.mjs
, and 4 sessions before
SIGTERM
, exit status 0, 0 after. No run on
main
or a release tag yet
|
| NuxtHub KV as a Nitro storage mount | Not verified; storage tests use Unstorage 1.17.3's memory driver |
| Nitro presets other than Node (serverless, edge, Workers) | Not tested; streaming needs a Node HTTP response |
| Linux beyond the CI checks above, Windows | Not run |
| Nix / NixOS | No Nix profile exists yet |
| Other Scala, Kyo, ZIO or h3 versions | Not supported; Kyo 1.0.0-RC7 needs Scala 3.9.0 and JDK 25 |
| Component | Version |
|---|---|
| Artifacts |
eleven
dev.yuri4n
modules,
0.1.0-alpha.1
, GitHub Packages (public)
|
| Scala | 3.5.2 for native and ZIO modules; 3.9.0 for Kyo modules |
| Scala.js / Kyo / ZIO | 1.22.0 / 1.0.0-RC7 / 2.1.23 |
| Build JDK / sbt | 25 / 1.11.7 |
| npm tooling |
@yuri4n/h34s-nuxt@0.1.0-alpha.1
, Node 22.18 or later, Nuxt 4.3.1 or 4.4.2
|
0.2.0-alpha.1
|
0.3.0-alpha.1
|
|---|---|
Kyo: return a
kyo.Stream[String, S]
(the
ResponseBody.stream
given)
|
Response.sendStream(stream)
, which gives
Sent < HandlerEffects
; return the
Sent
. The
ResponseBody.stream
given is removed
|
ZIO: return a
ZStream[Any, E, String]
(the
ZResponseBody.stream
and
infallibleStream
givens)
|
Response.sendStream(stream)
on
dev.yuri4n.exts.zioh3.Response
, which gives
ZIO[ZState[ResponseHead] & HandlerTransport, Throwable, Sent]
; return the
Sent
. Both givens are removed, with
StreamZResponseBody
, the public trait that held them
|
HandlerEffects = Async & Scope & Abort[Throwable] & Var[ResponseHead]
|
HandlerEffects
also has
Env[Fetch & HandlerTransport]
, which h34s provides. Code that passes a program to
KHandler.define
compiles unchanged
|
HandlerEnv = ZState[ResponseHead] & Scope
|
HandlerEnv = ZState[ResponseHead] & Scope & HandlerTransport & Fetch
, which h34s provides
|
Test with
Var.runTuple
or
ZIO.stateful
and a
Scope
|
KHandler.test(handler)(request)
or
ZHandler.test(handler)(request)
, which give a
TestResponse
. The manual provisioning no longer compiles: it lacks the transport, which only h34s can build
|
0.2.0-alpha.1
|
0.3.0-alpha.1
|
|---|---|
No server-side
$fetch
(
0.1.0-alpha.1
had
ZHandler.$fetch
and
SH3Event.$fetch
)
|
KHandler.$fetch
(Kyo, through
Env[Fetch & HandlerTransport]
) and
ZHandler.$fetch
(ZIO, through the
Fetch
of
HandlerEnv
).
raw
gives
FetchResponse.Plain
or
FetchResponse.Streamed
|
HandlerEnv = ZState[ResponseHead] & Scope
|
HandlerEnv = ZState[ResponseHead] & Scope & HandlerTransport & Fetch
. Code that passes a program to
ZHandler.define
compiles unchanged
|
| No test helper |
ZHandler.test(handler)(request)
keeps working;
ZHandler.test(handler)(request, fetch)
answers
$fetch
calls, for example with
Fetch.fake
, as
KHandler.test(handler)(request, fetch)
does
|
| 0.1.0-alpha.1 | Now | |
|---|---|---|
| Scala | 3.5.2 for native and ZIO modules, 3.9.0 for Kyo | 3.9.0 for every module |
| Build JDK | 21 was enough for ZIO-only builds | 25 for every build (Kyo 1.0.0-RC7 macros) |
| ZIO | 2.1.23 | 2.1.26 (pinned by kyo-zio 1.0.0-RC7) |
| zio-json | as resolved | 0.7.44 |
| Kyo | 1.0.0-RC7 | 1.0.0-RC7 |
| Scala.js, NativeConverter, h3 | 1.22.0, 0.9.0, 1.15.4 | unchanged |
| Module | Change |
|---|---|
zio-js-ext
|
Removed. It was an empty package. Delete the dependency |
nuxthub-ext
|
Removed.
HubKv
and
NativeKvSetOptions
are gone; storage comes from
KHubKv.mount
/
ZHubKv.mount
|
zio-h3-schema-ext
|
Removed.
Use
native-converter-zio-schema-ext
with
request.body.json[A]
(step 3)
|
native-converter-kyo-schema-ext
|
New.
Kyo Schema bridge:
derives Schema, NativeSchema
|
zio-h3-ext
|
Now depends on
kyo-h3-ext
and kyo-zio. ZIO applications get Kyo transitively
|
zio-nuxthub-ext
|
Now depends on
kyo-nuxthub-ext
and
zio-h3-ext
|
native-converter-zio-ext
,
native-converter-zio-schema-ext
|
Now depend on
h3-ext
|
h3-ext
|
No longer exposes ScalablyTyped-generated h3 facades. The facades are internal |
| 0.1.0-alpha.1 | Now |
|---|---|
KHandler.define(program)
with a by-name
A < HandlerEffects
|
KHandler.define(request => program)
|
KHandler.define[A: NativeConverter]
|
KHandler.define[B: ResponseBody]
; any
NativeConverter
value,
Unit
and
kyo.Stream[String, S]
are bodies
|
HandlerEffects
with
Env[KH3Event]
|
HandlerEffects = Async & Scope & Abort[Throwable] & Var[ResponseHead]
|
KH3Event
,
KHandler.getEvent
,
rawEvent
|
Removed.
Use the
Request
argument
|
KHandler.readBody[A]
,
KH3Event.readBody[A]
|
request.body.json[A]
|
KHandler.setResponseStatus(code)
,
KH3Event.setResponseStatus
|
Response.status(Status.Created)
or
Response.status(Status.unsafe(code))
|
dev.yuri4n.exts.kyoh3.SafeError
|
Same name, now defined in
h3-ext
and re-exported. It adds
headers
and
errors
|
KHubKv(native)
|
KHubKv.mount(base)
|
CommonEvHandler
|
EventHandler
(the old alias is deprecated)
|
| 0.1.0-alpha.1 | Now |
|---|---|
ZHandler.define(program)
with
ZIO[SH3Event & Scope & R, E, A]
|
ZHandler.define(request => program)
with
ZIO[HandlerEnv, ErrorResponse | Throwable, B]
|
SH3Event
,
ZHandler.getEvent
|
Removed.
Use the
Request
argument;
HandlerEnv = ZState[ResponseHead] & Scope
|
ZHandler[R, E, A]
,
Handler
,
TaskHandler
,
URHandler
,
UHandler
,
RHandler
|
Same names, now over
HandlerEnv
instead of
SH3Event
|
ZHandler.readBody[A]
,
readBody[A](strict)
,
SH3Event.readBody
|
request.body.json[A]
. Parsing is always strict; a non-JSON content type is a 415
|
ZHandler.getQuery
,
getQuery[T](using Conversion)
|
request.query
,
request.queryAll
|
ZHandler.getRouterParams
,
getRouterParam(name)
|
request.routerParams
,
request.routerParam(name)
, or
request.param(name)
(400 when absent)
|
GetRouterParamsError
,
SH3Event.MissingRouterParamError
|
Removed.
A missing parameter is
HttpError(400, "Missing router parameter:
|
SH3Event.parseCookies
|
request.cookies
,
request.cookie(name)
|
ZHandler.setResponseStatus(code)
|
Response.status(...)
|
ZHandler.sendStream(stream)
,
SH3Event.sendStream
|
Return the
ZStream
as the body. Elements are strings now:
sendStream
converted each element with its
NativeConverter
, which Node cannot write. Encode values yourself, for example
stream.map(_.toJson + "\n")
|
respondWith
,
respondWithZIO
|
Removed.
Return the body; set the head with
Response.*
|
ZHandler.$fetch
,
SH3Event.$fetch
|
Removed. Call your service directly, or use an HTTP client |
mkHandler
, top-level
defineEventHandler(h)
and
defineEventHandler(runtime, h)
|
ZHandler.define
. Put a
given Runtime[Any]
in scope for a custom runtime
|
ZIOH3.Console
|
Removed.
Use ZIO's
Console
or
ZIO.log
|
taskStreamToReadableStream
,
taskStreamToReadableStreamReader
,
streamconversion
|
Removed. Streams are bodies now |
fromNative_
extension on
js.Any
|
Removed.
Decode request bodies with
request.body.json[A]
|
ErrorResponse(errors)
|
ErrorResponse(errors, status = 400)
. A one-field pattern match needs
_
; its
JsonEncoder
output now has
status
|
ZIO
SafeError
with
val message
,
status
,
maybeUnderlying
|
The shared
SafeError
(
message
,
status
,
headers
,
errors
).
maybeUnderlying
is gone; a
val message
still works
|
ZHubKv(native)
|
ZHubKv.mount(base)
|
ZHandler.readBodyWithSchema[A]
,
InvalidBody
(zio-h3-schema)
|
derives Schema, NativeSchema
on the model, then
request.body.json[A]
; failures are
InvalidInput
|
NativeSchema(schema).converter
returned a converter that failed with a generic error
|
It now throws
InvalidInput
with entries;
Codec
gains
validate
;
derives NativeSchema
is new
|
| Response | 0.1.0-alpha.1 | Now (default options) |
|---|---|---|
| A value | The value as is |
{"data":
|
Unit
|
undefined
(h3 decided the status)
|
204 with no body |
| A stream |
Through
sendStream
|
The stream of strings as the body, as UTF-8 text;
text/plain; charset=utf-8
unless you set a content type
|
| A safe error |
{"errors": ["message"]}
|
{"errors": [{"message": "..."}]}
, with
path
for schema entries
|
| An internal error |
{"errors": ["Internal server error"]}
, plus top-level
debug
in development
|
{"errors": [{"message": "Internal server error"}]}
; in development or debug mode the entry carries
debug.cause
|