
# Nitro

> Nitro 3 runs on h3 v2 and hands handlers the same `H3Event`, which goes to `use()` as it is. Nitro has no auto-imports, and its own `h3` is not your dependency: everything comes from `nitro` and `nitro/h3`.

```ts
// server/utils/session.ts
import { HTTPError } from "nitro";
import { type WebSessionEvent, defineJWESession, deriveJWKFromPassword } from "unjwt/session";

const key = await deriveJWKFromPassword(process.env.SESSION_PASSWORD!, {
  alg: "PBES2-HS256+A128KW",
  salt: "my-app-session",
  iterations: 600_000,
});

const definition = defineJWESession<{ user: string }, "1h">({ key, maxAge: "1h", rolling: "15m" });

export const useAppSession = (event: WebSessionEvent) => definition.use(event);

export async function requireAppSession(event: WebSessionEvent) {
  const current = await useAppSession(event);
  if (!current.id) throw new HTTPError({ status: 401, message: "Sign in first" });
  return current;
}
```

```ts
// server/routes/login.post.ts
import { HTTPError, defineHandler } from "nitro";
import { readBody } from "nitro/h3";
import { useAppSession } from "../utils/session.ts";

export default defineHandler(async (event) => {
  const body: { user?: unknown } | undefined = await readBody(event);
  if (typeof body?.user !== "string")
    throw new HTTPError({ status: 400, message: "user required" });
  const session = await useAppSession(event);
  await session.update({ user: body.user });
  return { id: session.id };
});
```

`defineHandler`, `defineWebSocketHandler` and `HTTPError` come from `nitro`, `readBody` and the other h3 helpers from `nitro/h3`, and the session utils are imported by path.

## A handler that throws after a write

Nitro's error handler answers with a `Response` of its own, which h3 sends without the handler's prepared headers. A `clear()` followed by a thrown `401` therefore leaves the client signed in, and the `errHeaders` route that works on bare h3 makes no difference here. The next request that opens the session drops the cookie.

## WebSockets

The `upgrade` hook receives a `Request` with an optional `context`; a thrown `Response` refuses; the returned object carries `headers` for the `101` handshake and `context` for every later hook's `peer.context`. The session is read once there, and nothing can be written to the client after the upgrade.

```ts
// server/routes/ws.ts
import { defineWebSocketHandler } from "nitro";
import { useAppSession } from "../utils/session.ts";

export default defineWebSocketHandler({
  async upgrade(request) {
    const headers = new Headers();
    const session = await useAppSession({
      headers: request.headers,
      context: request.context,
      res: { headers },
    });
    if (!session.id) throw new Response("Sign in first", { status: 401 });
    return { headers, context: { ...session } }; // a roll or a drop made during the read rides on the 101
  },
  open(peer) {
    peer.send(`hello ${(peer.context as { data: { user: string } }).data.user}`);
  },
});
```

The fields are named rather than spread because a spread drops a `Request`'s `headers` getter. crossws 0.4 merges the returned headers with `set`, which keeps one `Set-Cookie` line: a session that fits one cookie is what rides the upgrade.

The `playgrounds/nitro` directory of the repository is this page as a running app, with a static `index.html` in front.
