# H3

> Add your API routes to an H3 v2 app.

Add your contract's routes to an H3 v2 app with `@hulla/api-h3`. The routes use H3's middleware and event context.

For Nuxt, use [`@hulla/api-nuxt`](https://hulla.dev/docs/api/full-stack/nuxt.md), which supports Nuxt's H3 v1 integration.

## Install the adapter

The adapter requires Node.js 22 or later and supports H3 `^2.0.1-rc.29`. Install that H3 v2 range explicitly.
This example uses Zod for request and response schemas.

### Run with bun

```sh
bun add @hulla/api @hulla/api-h3 h3@^2.0.1-rc.29 zod
```

### Run with npm

```sh
npm install @hulla/api @hulla/api-h3 h3@^2.0.1-rc.29 zod
```

### Run with pnpm

```sh
pnpm add @hulla/api @hulla/api-h3 h3@^2.0.1-rc.29 zod
```

### Run with yarn

```sh
yarn add @hulla/api @hulla/api-h3 h3@^2.0.1-rc.29 zod
```

## Mount a route and verify it

Declare the HTTP boundary in a module shared by server and client code:

```ts
// src/api/contract.ts
import { defineContract, request, response, route } from '@hulla/api'
import { z } from 'zod'

export const contract = defineContract({
  basePath: '/api',
  routes: {
    renameUser: route.patch('/users/:id', {
      params: z.object({ id: z.string() }),
      body: request.json(z.object({ name: z.string().min(1) })),
      responses: {
        200: response.json(
          z.object({
            id: z.string(),
            name: z.string(),
            actor: z.string(),
          })
        ),
      },
    }),
  },
})
```

Implement the route in server-only code. The handler receives decoded, validated inputs, and `response()` only
accepts a status and body declared by the contract:

```ts
// src/api/server.ts
import { defineServer } from '@hulla/api/server'
import { contract } from './contract'

export const implementation = defineServer(contract).implement({
  renameUser: ({ params, body, response }) =>
    response(200, {
      id: params.id,
      name: body.name,
      actor: 'api',
    }),
})
```

Create the H3 app, register host middleware, and mount the implementation:

```ts
// src/app.ts
import { h3Adapter } from '@hulla/api-h3'
import { H3 } from 'h3'
import { implementation } from './api/server'

export const app = new H3()

app.use(async (_event, next) => {
  const result = await next()
  if (result instanceof Response) {
    result.headers.set('x-host', 'h3')
  }
  return result
})

const adapter = h3Adapter(app)
adapter.mount(implementation)
```

`mount()` registers every selected contract route through H3's native `app.on()` API and returns that same
`app`. Verify the integration without opening a network port by using H3's request helper:

```ts
// scripts/verify-api.ts
import { app } from '../src/app'

const result = await app.request('/api/users/user%201', {
  method: 'PATCH',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ name: 'Grace' }),
})

console.log(result.status) // 200
console.log(result.headers.get('x-host')) // h3
console.log(await result.json())
// { id: 'user 1', name: 'Grace', actor: 'api' }
```

The request enters H3's router, runs the host middleware and contract handler, and returns a standard Web
`Response`. In a Node.js entry point, pass the same app to H3's server:

```ts
import { serve } from 'h3'
import { app } from './app'

serve(app)
```

Other H3 runtime adapters can consume `app.fetch` or the app itself. `@hulla/api-h3` does not choose a deployment
target or start a server.

## Keep H3 routing and middleware

> Register H3 middleware before `mount()` when it must wrap a contract route. H3 uses the app's native middleware
> and router order.

For a matched contract route:

1. H3 matches the method and path, then enters middleware registered before the route.
2. The adapter reads path parameters, the URL query, headers, and the declared body representation.
3. `@hulla/api` validates those values, creates server context, runs its middleware, and calls the handler.
4. The Web `Response` returns through the remaining H3 middleware.

Native H3 behavior still applies:

- unmatched requests continue to other H3 routes and not-found handling;
- mounting a [server fragment](https://hulla.dev/docs/api/core/server-composition.md) registers only its selected routes;
- a `HEAD` request can use a matching contract `GET` route, with H3 suppressing the body;
- custom contract `QUERY` routes are registered as native H3 `QUERY` routes;
- repeated query values are preserved from `event.url.searchParams`.

Use the contract handler's `params` for decoded and validated values. H3 keeps wire-form router parameters on
`event.context.params`: for example, `user%201` becomes `user 1` in the contract handler. H3's safe decoder
deliberately preserves an encoded slash as `%2F`. Avoid slash-containing identifiers when one contract must produce
identical parameter values across different native routers.

## Choose who reads the request body

A Web request body can be consumed once. Choose the setup based on which H3 layer needs it:

| Readers                                          | Setup                                      | Result                                                                                          |
| ------------------------------------------------ | ------------------------------------------ | ----------------------------------------------------------------------------------------------- |
| Contract route only                              | Use the default mount                      | The adapter reads `event.req` in the contract's JSON, text, bytes, or form-data representation. |
| H3 middleware and contract route                 | Read `event.req.clone()` in the middleware | Middleware consumes the clone; the original remains available to the adapter.                   |
| Contract route and adapter-bound context factory | Set `preserveRequestBody: true`            | The adapter decodes a clone; the context factory can read the original `request`.               |

Reading `event.req` directly in earlier middleware consumes the original stream and cannot be undone later. Clone it
before the first read when another layer still needs the body.

The adapter imposes no body-size limit by default. Set `maxBodyBytes` to bound its read; H3, a proxy, or the
deployment runtime may enforce separate limits:

```ts
const adapter = h3Adapter(app, { maxBodyBytes: 1_048_576 })
adapter.mount(implementation, { preserveRequestBody: true })
```

Adapter defaults apply to every mount. Options passed to `mount()` shallowly override those defaults for that
implementation or fragment.

## Add H3 event state to server context

Keep the context factory portable when it only needs application services or route metadata. Use
`adapter.context()` when handlers need H3 event context, the prepared response, runtime details, or `waitUntil()`:

```ts
import { defineServer } from '@hulla/api/server'
import { h3Adapter } from '@hulla/api-h3'
import { H3 } from 'h3'
import { contract } from './api/contract'

const app = new H3()
app.use((event) => {
  event.context['actor'] = 'Ada'
})

const adapter = h3Adapter(app)
const implementation = defineServer(contract, {
  context: adapter.context(({ request, h3Event, route }) => ({
    actor: String(h3Event.context['actor']),
    requestId: request.headers.get('x-request-id'),
    routeKey: route.key.join('.'),
  })),
}).implement({
  renameUser: ({ params, body, context, response }) => {
    console.info({
      requestId: context.requestId,
      route: context.routeKey,
    })
    return response(200, {
      id: params.id,
      name: body.name,
      actor: context.actor,
    })
  },
})

adapter.mount(implementation)
```

The factory receives:

- `request`: the Web `Request` exposed by `h3Event.req`;
- `h3Event`: H3's native event, including mutable `context`, parsed URL, prepared response, runtime details, and
  `waitUntil()`;
- `route`: the contract route key, HTTP method, and full path.

`adapter.context()` binds the implementation to the H3 adapter. Mounting it through Fetch, another framework
adapter, or the in-process transport fails immediately. Context code should return values for the server handler
rather than finalizing a separate H3 response.

## Separate adapter failures from H3 failures

Use `onError` for failures while the adapter decodes contract inputs, creates context, runs `@hulla/api`
middleware or the handler, serializes the declared result, or converts it to a Web `Response`:

```ts
const adapter = h3Adapter(app, {
  onError({ error, phase, request, h3Event, defaultResponse }) {
    console.error(phase, request.url, h3Event.url.pathname, error)
    return defaultResponse
  },
})
```

The hook receives the H3 event and a protocol-safe default `Response`. Return another `Response` to replace it, or
return `undefined` to keep the default. Options passed to `mount()` override adapter defaults for that mount;
`onError: undefined` disables an inherited hook.

H3's error pipeline owns failures from H3 middleware and native routes outside the mounted adapter. If a stream
producer fails after its response has started, `onError` can observe the transport failure but cannot replace the
response already in flight.

## Handle streams and request cancellation

Declared stream bodies must be iterable or async iterable and yield `Uint8Array` chunks. The adapter exposes the
source as a Web `ReadableStream` without collecting the complete body first. Finishing, cancelling, or failing the
stream closes its source iterator.

The native request's `AbortSignal` becomes the handler's `signal`. When the H3 runtime aborts that request signal,
the handler can use it to stop database, file, or network work.

## Next steps

- [Server context and middleware](https://hulla.dev/docs/api/core/server-context.md) explains portable request context and route policies.
- [Server fragments and composition](https://hulla.dev/docs/api/core/server-composition.md) shows how to mount selected route groups.
- [Calling the API](https://hulla.dev/docs/api/core/clients.md) explains how a browser or another service calls the same contract.
- [Nuxt](https://hulla.dev/docs/api/full-stack/nuxt.md) covers the dedicated H3 v1 integration for Nuxt applications.
- [H3 documentation](https://h3.dev/) covers native routing, middleware, and deployment.
