Migration guide for v1 to v2

H3 version 2 includes some behavior and API changes that you need to consider applying when migrating.

Currently H3 v2 in beta stage. You can try with nightly channel.
This is an undergoing migration guide and might be updated.
H3 has a brand new documentation rewrite. Head to the new Guide section to learn more!

Latest Node.js and ESM-only

H3 v2 requires Node.js >= 20.11 (latest LTS recommended) .

If your application is currently using CommonJS modules (require and module.exports), You can still use require("h3") thanks to require(esm) supported in latest Node.js versions.

You can alternatively use other compatible runtimes Bun or Deno.

Web Standards

H3 v2 is rewritten based on web standard primitives (URL, Headers, Request, and Response).

When using Node.js, H3 uses a compatibility layer (💥 srvx) and in other runtimes uses native web compatibility APIs.

Access to the native event.node.{req,res} is only available when running server in Node.js runtime.

event.web is renamed to event.req (instance of web Response).

Response Handling

You should always explicitly return the response body or throw an error.

If you were previously using methods below, you can replace them with return statements returning a text, JSON, stream, or web Response (h3 smartly detects and handles each):

  • send(event, value): Migrate to return <value>.
  • sendError(event, <error>): Migrate to throw createError(<error>).
  • sendStream(event, <stream>): Migrate to return <stream>.
  • sendWebResponse(event, <response>): Migrate to return <response>.

Other send utils that are renamed and need explicit return:

  • sendNoContent(event) / return null: Migrate to return noContent(event).
  • sendIterable(event, <value>): Migrate to return iterable(event, <value>).
  • sendProxy(event, target): Migrate to return proxy(event, target).
  • handleCors(event): Check return value (boolean) and early return if handled.
  • serveStatic(event, content): Make sure to add return before.
  • sendRedirect(event, location, code): Migrate to return redirect(event, location, code).
Read more in Sending Response.

H3 and Router

Router function is now integrated into the H3 core.
Instead of createApp() and createRouter() you can use new H3().

Any handler can return a response. If middleware don't return a response, next handlers will be tried and finally make a 404 if neither responses. Router handlers can return or not return any response, in this case, H3 will send a simple 200 with empty content.

Read more in Request Lifecycle.

H3 migrated to a brand new route-matching engine (🌳 rou3). You might experience slight (but more intuitive) behavior changes for matching patterns.

Other changes from v1:

  • Middleware added with app.use("/path", handler) only matches /path (not /path/foo/bar). For matching all subpaths like before, it should be updated to app.use("/path/**", handler).
  • The event.path received in each handler will have a full path without omitting the prefixes. use withBase(base, handler) utility to make prefixed app. (example: withBase("/api", app.handler)).
  • router.add(path, method: Method | Method[] signature is changed to router.add(method: Method, path)
  • router.use(path, handler) is deprecated. Use router.all(path, handler) instead.
  • app.use(() => handler, { lazy: true }) is no supported anymore. Instead you can use app.use(defineLazyEventHandler(() => handler), { lazy: true }).
  • app.use(["/path1", "/path2"], ...) and app.use("/path", [handler1, handler2]) are not supported anymore. Instead, use multiple app.use() calls.
  • app.resolve(path) removed.
Read more in Routing.
Read more in Middleware.

Request Body

Most of request body utilities can now be replaced with native event.req.* methods which is based on web Request interface.

readBody(event) utility will use JSON.parse or URLSearchParams for parsing requests with application/x-www-form-urlencoded content-type.

Behavior changes:

  • Body utils won't throw an error if the incoming request has no body (or is a GET method for example) but instead, return empty values.
  • Native request.json and readBody does not use unjs/destr anymore. You should always filter and sanitize data coming from user to avoid prototype-poisoning.
H3 now natively uses standard web Headers for all utils.

Header values are always a plain string now (no null or undefined or number or string[]).

For the Set-Cookie header, you can use headers.getSetCookie that always returns a string array.

Other Deprecations

H3 v2 deprecated some legacy and aliased utilities.

App and router utils

  • createApp / createRouter: Migrate to new H3().

Error utils

  • createError/H3Error: Migrate to HTTPError
  • isError: Migrate to HTTPError.isError

Handler utils

  • eventHandler/defineEventHandler: Migrate to defineHandler (you can also directly use a function!).
  • lazyEventHandler: Migrate to defineLazyEventHandler.
  • toEventHandler: Remove wrapper.
  • isEventHandler: (removed) Any function can be an event handler.
  • useBase: Migrate to withBase.
  • defineRequestMiddleware and defineResponseMiddleware removed.

Request utils

  • getHeader / getRequestHeader: Migrate to event.req.headers.get(name).
  • getHeaders / getRequestHeaders: Migrate to Object.fromEntries(event.req.headers.entries()).
  • getRequestPath: Migrate to event.path or event.url.
  • getMethod: Migrate to event.method.

Response utils

  • getResponseHeader / getResponseHeaders: Migrate to event.res.headers.get(name)
  • setHeader / setResponseHeader / setHeaders / setResponseHeaders: Migrate to event.res.headers.set(name, value).
  • appendHeader / appendResponseHeader / appendResponseHeaders: Migrate to event.res.headers.append(name, value).
  • removeResponseHeader / clearResponseHeaders: Migrate to event.res.headers.delete(name)
  • appendHeaders: Migrate to appendResponseHeaders.
  • defaultContentType: Migrate to event.res.headers.set("content-type", type)
  • getResponseStatus / getResponseStatusText / setResponseStatus: Use event.res.status and event.res.statusText.

Node.js utils

  • defineNodeListener: Migrate to defineNodeHandler.
  • fromNodeMiddleware: Migrate to fromNodeHandler.
  • toNodeListener: Migrate to toNodeHandler.
  • createEvent: (removed): Use Node.js adapter (toNodeHandler(app)).
  • fromNodeRequest: (removed): Use Node.js adapter (toNodeHandler(app)).
  • promisifyNodeListener (removed).
  • callNodeListener: (removed).

Web Utils

  • fromPlainHandler: (removed) Migrate to Web API.
  • toPlainHandler: (removed) Migrate to Web API.
  • fromPlainRequest (removed) Migrate to Web API or use mockEvent util for testing.
  • callWithPlainRequest (removed) Migrate to Web API.
  • fromWebRequest: (removed) Migrate to Web API.
  • callWithWebRequest: (removed).

Body Utils

  • readRawBody: Migrate to event.req.text() or event.req.arrayBuffer().
  • getBodyStream / getRequestWebStream: Migrate to event.req.body.
  • readFormData / readMultipartFormData / readFormDataBody: Migrate to event.req.formData().

Other Utils

  • isStream: Migrate to instanceof ReadableStream.
  • isWebResponse: Migrate to instanceof Response.
  • splitCookiesString: Use splitSetCookieString from cookie-es.
  • MIMES: (removed).

Type Exports

There might be more type changes.
  • App: Migrate to H3.
  • AppOptions: Migrate to H3Config.
  • _RequestMiddleware: Migrate to RequestMiddleware.
  • _ResponseMiddleware: Migrate to ResponseMiddleware.
  • NodeListener: Migrate to NodeHandler.
  • TypedHeaders: Migrate to RequestHeaders and ResponseHeaders.
  • HTTPHeaderName: Migrate to RequestHeaderName and ResponseHeaderName.
  • H3Headers: Migrate to native Headers.
  • H3Response: Migrate to native Response.
  • MultiPartData: Migrate to native FormData.
  • RouteNode: Migrate to RouterEntry. CreateRouterOptions: Migrate to RouterOptions.

Removed type exports: WebEventContext, NodeEventContext, NodePromisifiedHandler, AppUse, Stack, InputLayer, InputStack, Layer, Matcher, PlainHandler, PlainRequest, PlainResponse, WebHandler.