|claim|login
RepoCritics — Review. Share. Archive. Every open-source repo.

h3js/h3

Wiki: h3js/h3

Source: https://github.com/h3js/h3

Last synced 2026-07-16 · 1146 words · Edit wiki on GitHub →

h3js/h3

> Minimal, composable H(TTP) framework built for high performance and cross-runtime portability.

GitHub repo · Official website · License: MIT

Overview

H3 (pronounced "h-3") is a small HTTP server framework authored by Pooya Parsa (pi0) as part of the UnJS ecosystem1. It was created in 2020 as the server layer that would eventually power Nitro — the server engine underneath Nuxt 3 — and it is still most widely deployed transitively through that stack rather than as a directly-chosen dependency. The repository moved from the unjs org to its own h3js org and the h3.dev domain during the v2 cycle.

The defining design choice is composability over convention. Instead of a large application object with dozens of built-in methods, H3 ships a flat set of tree-shakeable utility functions (readBody, getQuery, setCookie, sendRedirect, and so on) that operate on a single H3Event object. You import only what a given handler uses, so a minimal app bundles to a few kilobytes — which matters because H3 targets serverless and edge runtimes where cold-start size is a real cost.

The other defining trait is runtime portability. H3 is written to run on Node.js, Deno, Bun, and Web-standard environments (Cloudflare Workers, Vercel Edge, service workers) from the same handler code. The central tension in the project is the v1→v2 transition this portability forced: v1 was built around Node's IncomingMessage/ServerResponse, while v2 is a rewrite around the Web Request/Response standard2. The two lines are not source-compatible, and as of 2026 both are in active use.

Getting Started

npm install h3
# or: pnpm add h3 / bun add h3 / deno add npm:h3
// server.ts — v2 (web-standard) style
import { H3, serve, readBody } from "h3";

const app = new H3();

app.get("/", () => ({ hello: "world" }));

app.post("/echo", async (event) => {
  const body = await readBody(event);
  return { received: body };
});

serve(app, { port: 3000 });

Handlers return plain values — objects are serialized to JSON, strings are sent as-is, and returning a Response gives full control. There is no res.send() ceremony for the common case.

Architecture / How It Works

At the center is the H3Event object, a per-request context wrapping the incoming request, the eventual response, and shared state. Utility functions take an event and read from or mutate it; this is what makes the API tree-shakeable — the framework is a bag of functions, not a class hierarchy.

App and routing. An app is a stack of middleware and a router. In v1 the primary entry was createApp() + createRouter(); v2 folds routing into the H3 app class with .get()/.post()/... methods and .use() for middleware. Route matching is delegated to a separate UnJS package — radix3 in the v1 line, replaced by rou3 in v2 — rather than implemented inline. Middleware are just event handlers that run before the matched route and can short-circuit by returning a value.

Handlers. A handler is any function (event) => value. Its return value drives the response: a plain object becomes JSON, a string becomes text/html, undefined/null becomes an empty response, a thrown HTTPError (or createError) becomes a structured error response, and a returned web Response is passed through untouched.

Runtime adapters. This is where v1 and v2 differ most. v1 exposed toNodeListener(app) to bridge into node:http, plus web/Node adapter helpers for other targets. v2 standardizes on the Web Request/Response interface internally and uses srvx — a separate UnJS server-adapter layer — to run the same app on Node, Deno, and Bun with runtime-native performance. On Node this still means a compatibility shim between node:http streams and web Request/Response; the abstraction is not free, but it is centralized.

The framework deliberately owns very little: no built-in ORM, template engine, validation, or auth. Body parsing, cookies, CORS, and proxying ship as utilities; everything else is expected to come from the surrounding ecosystem (Nitro, or your own composition).

Production Notes

  • You are probably running H3 without choosing it. Most production H3 usage arrives through Nitro/Nuxt. If you hit an H3 behavior you dislike, check whether Nitro is wrapping or overriding it before filing against H3 directly — the effective request lifecycle in a Nuxt app includes Nitro's own middleware and route rules.
  • v1 vs v2 is a hard fork in practice. The main branch is v2; v1 lives on the v1 branch and v1.h3.dev. Utility signatures, the app constructor, error handling, and the request/response model all changed. Do not assume a v1 tutorial's code runs on v2. Pin your major version and read the v2 migration notes before upgrading.
  • Web-standard model has Node edge cases. Because v2 models everything as web Request/Response, streaming bodies, content-length handling, and raw socket access behave differently than the v1 node:http path. Code that reached into the underlying Node req/res object needs rework.
  • Error handling is explicit. Use createError/HTTPError with a statusCode to produce proper HTTP error responses; a bare thrown Error becomes a 500. Unhandled async rejections in handlers surface as 500s, so validate and wrap external calls.
  • Tree-shaking depends on your bundler. The size advantage only materializes if you import named utilities (import { readBody } from "h3") and bundle with a tree-shaking-aware tool. Namespace imports defeat it.
  • Ecosystem maturity is uneven across runtimes. Node and the Nitro-blessed path are the most exercised; Deno/Bun/edge targets work but see less production mileage, so validate your specific runtime under load rather than assuming parity.

When to Use / When Not

Use when:

  • You want a tiny, composable HTTP layer that runs unchanged on Node, Bun, Deno, and edge runtimes.
  • You care about cold-start bundle size (serverless / edge functions).
  • You are already in the UnJS/Nitro/Nuxt ecosystem and want consistent primitives.
  • You prefer importing utilities over inheriting a large framework object.

Avoid when:

  • You want a large, batteries-included framework with built-in ORM/auth/validation conventions — H3 intentionally omits these.
  • You need a big library of ready-made third-party middleware; Express's ecosystem is far larger.
  • You are pinned to Node-specific req/res internals that the v2 web-standard model abstracts away.
  • You need long-term API stability today — the v1→v2 transition means the surface is still consolidating.

Alternatives

  • honojs/hono — the closest competitor; web-standard, multi-runtime HTTP framework with a larger standalone community. Use Hono when you want a multi-runtime framework with its own mature middleware ecosystem rather than the UnJS/Nitro lineage.
  • expressjs/express — use when you want the largest middleware ecosystem and Node-centric ubiquity, and don't need edge/web-standard portability.
  • fastify/fastify — use when raw Node throughput and schema-based validation/serialization are the priority.
  • elysiajs/elysia — use when you are Bun-first and want end-to-end type inference as a core feature.
  • koajs/koa — use when you want a minimal Node middleware core in the Express lineage with async/await ergonomics.

History

VersionDateNotes
initial2020-11Repository created under the unjs org as the HTTP layer for Nitro1.
v1.x2022–2024Stable line built on node:http (IncomingMessage/ServerResponse); createApp + radix3 router. Powers Nitro/Nuxt 3.
v2 (active)2025–2026Rewrite around Web Request/Response; H3 app class, rou3 router, srvx runtime adapters. Repo moved to h3js org and h3.dev2.

References

  1. ^ H3 documentation and UnJS project. https://h3.dev
  2. ^ H3 README, v2 active-branch note and migration guidance (v1 on the v1 branch / v1.h3.dev). https://github.com/h3js/h3

Tags

typescript, http-framework, web-framework, serverless, edge, nodejs, bun, deno, unjs, nitro, cross-runtime, minimal