Thanks to visit codestin.com
Credit goes to h3.dev

Routing

Each request is matched to one (most specific) route handler.

Adding Routes

You can register route handlers to H3 instance using H3.on, H3.[method], or H3.all.

Router is powered by 🌳 Rou3, an ultra-fast and tiny route matcher engine.

Example: Register a route to match requests to the /hello endpoint with HTTP GET method.

  • Using H3.[method]
    app.get("/hello", () => "Hello world!");
    
  • Using H3.on
    app.on("GET", "/hello", () => "Hello world!");
    

You can register multiple event handlers for the same route with different methods:

app
  .get("/hello", () => "GET Hello world!")
  .post("/hello", () => "POST Hello world!")
  .all("/hello", () => "Any other method!");

You can also use H3.all method to register a route accepting any HTTP method:

app.all("/hello", (event) => `This is a ${event.req.method} request!`);

HEAD Requests

Following RFC 9110, HEAD requests automatically match the corresponding GET route and run its handler, but the response body is omitted (only the headers and status are sent). You don't need to register a separate HEAD handler:

app.get("/hello", () => "Hello world!");

// HEAD /hello → 200 with the same headers as GET, but an empty body

Register an explicit HEAD handler when you want to override this — for example, to skip computing the body:

app.head("/hello", (event) => {
  event.res.headers.set("content-length", "12");
  return null;
});

An explicit head() route always takes precedence over the automatic GET fallback.

HTTP QUERY Method

H3 supports the HTTP QUERY method (RFC 10008) as a first-class method. QUERY is like GETsafe, idempotent, and cacheable — but carries a request body (with a Content-Type), closing the long-standing "GET with a body" gap. It's ideal for complex read operations where filters don't fit in a URL.

Register a QUERY handler with app.query() (or app.on("QUERY", …)) and read the request body as usual:

import { readBody } from "h3";

app.query("/search", async (event) => {
  const criteria = await readBody(event); // read the query body
  return runSearch(criteria);
});

Because QUERY carries an attacker-controllable body, body-size limits apply just like POST.

Two utilities help implement the RFC:

QUERY is treated like GET for conditional caching (304 responses via handleCacheHeaders), and proxy forwards it with its body. Unlike GET, QUERY is not CORS-safelisted, so browsers send a preflight — if you pass an explicit methods allowlist to handleCors, include "QUERY".
See the HTTP QUERY method example for a runnable /books resource that validates the Content-Type and advertises a cacheable GET alternative.

Dynamic Routes

You can define dynamic route parameters using : prefix:

// [GET] /hello/Bob => "Hello, Bob!"
app.get("/hello/:name", (event) => {
  return `Hello, ${event.context.params.name}!`;
});

Instead of named parameters, you can use * for unnamed optional parameters:

app.get("/hello/*", (event) => `Hello!`);

Wildcard Routes

Adding /hello/:name route will match /hello/world or /hello/123. But it will not match /hello/foo/bar. When you need to match multiple levels of sub routes, you can use ** prefix:

app.get("/hello/**", (event) => `Hello ${event.context.params._}!`);

This will match /hello, /hello/world, /hello/123, /hello/world/123, etc.

Param _ will store the full wildcard content as a single string.

Route Meta

You can define optional route meta when registering them, accessible from any middleware.

import { H3 } from "h3";

const app = new H3();

app.use((event) => {
  console.log(event.context.matchedRoute?.meta); // { auth: true }
});

app.get("/", (event) => "Hi!", { meta: { auth: true } });
It is also possible to add route meta when defining them using defineHandler object syntax.