Guide

Event Handler

Event handler defines application logic.

After creating an h3 app instance, you can start defining your application logic using event handlers.

An event handler is a function that receives an H3Event context and returns a response.

Defining Event handlers

You can define typed event handlers using defineEventHandler.

Using defineEventHandler is optional. When using app methods to register routes or middleware, the type hints will be still available.
import { defineEventHandler } from "h3";

defineEventHandler((event) => "Response");

The callback function can be sync or async:

defineEventHandler(async (event) => "Response");

Object Syntax

You can use an object syntax in defineEventHandler for more flexible options.

defineEventHandler({
  onRequest: [],
  onBeforeResponse: []
  handler: () => "Response",
})

Responses Types

Values returned from event handlers are automatically converted to http response.

If returned value from event handler is a Promise or from an async function, h3 will wait for it to resolve before sending the response.

null or undefined

Sends a response with empty body in route handlers or goes to next handler for middleware handlers.

If there is no return statement in event handler, it is same as return undefined.

String

Returning a string value, sends it as plain text body.

If not setting content-type, it can default to text/plain;charset=UTF-8.

Example: Send HTML response

app.get("/", (event) => {
  event.res.headers.set("Content-Type", "text/html;charset=UTF-8");
  return "<h1>hello world</h1>";
});

JSON serializable value

Returning a JSON serializable value (object, array, number or boolean), it will be stringified using JSON.stringiy() and sent with default application/json content-type.

Example:

app.get("/", (event) => ({ hello: "world" }));
You can add toJSON() method in object to customize serialization behavior. Check MDN docs for more info.

Response

Send a standard web Response.

Example:

app.get("/", (event) => Response.json({ hello: "world" }));
When sending a Response, any headers that set using event.res.headers before, will be merged as default headers. event.res.{status/statusText} will be ignored.
For performance reasons, it is best to only set headers only from final Response.

ReadableStream or Readable

Send a standard web ReadableStream or node Readable.

ArrayBuffer or Uint8Array or Buffer

Send binary ArrayBuffer, Uint8Array or node Buffer.

content-length header will be automatically set.

Error

Retuning an Error instance will send it.

It is better to throw errors instead of returning them and using createError utility.

BigInt

Value will be sent as stringified version of BigInt number.

Returning a JSON object, does not allows BigInt serialization. You need to implement toJSON. Check MDN docs for more info.

Blob

Send a standard web Blob as stream.

Content-type and Content-Length headers will be automatically set.

Symbol or Function

Returning Symbol or Function has undetermined behavior. Current version sends a string-like representation of unknown Symbols and Functions but it might be changed to throw an error, never return them. There are some internal known Symbols h3 internally uses (can change in future):

  • Symbol.for("h3.notFound"): Indicate no route is found to throw a 404 error.
  • Symbol.for("h3.handled"): Indicate request is somehow handled and h3 should not continue (Node.js specific).

Error Handling

You can easily control the error returned by using the createError utility.

import { createError } from "h3";

app.get("/error", (event) => {
  throw createError({
    status: 400,
    statusMessage: "Bad Request",
    message: "Invalid user input",
    data: { field: "email" },
  });
});

This will end the request with 400 - Bad Request status code and the following JSON response:

{
  "statusCode": 400,
  "statusMessage": "Bad Request",
  "stack": [],
  "data": {
    "field": "email"
  }
}

String vs. Object Errors

When creating an error using createError, you also have the option to pass a string instead of an object. Doing so will set the message property of the error. In this case, the statusCode will default to 500.

import { createError } from "h3";

app.get("/error", (event) => {
  throw createError("An error occurred");
});
Typically, message contains a brief, human-readable description of the error, while statusMessage is specific to HTTP responses and describes the status text related to the response status code. In a client-server context, using a short statusMessage is recommended because it can be accessed on the client side. Otherwise, a message passed to createError on the server will not propagate to the client (you can use data instead). Consider avoiding to put dynamic user input to the message to avoid potential security issues.

Internal Errors

If during calling an event handler an error with new Error() will be thrown (without createError), h3 will automatically catch as a 500 - Internal Server Error status response considering it an unhandled error.

app.get("/error", (event) => {
  // Do NOT do this and use createError()!
  throw new Error("Something went wrong");
});

Lazy Handlers

You can define lazy event handlers using defineLazyEventHandler. This allow you to define some one-time logic that will be executed only once when the first request matching the route is received.

A lazy event handler must return an event handler:

import { defineLazyEventHandler } from "h3";

app.get(
  "/lazy",
  defineLazyEventHandler(() => {
    console.log("This will be executed only once");
    // This will be executed only once
    return (event) => {
      // This will be executed on every request
      return "Response";
    };
  }),
);

This is useful to define some one-time logic such as configuration, class initialization, heavy computation, etc.

Middleware Handlers

Event handlers that don't return any value act as middleware. They can be used to add side effects to your application such as logging, caching, etc or to modify the request or response.

Middleware handlers should be registered using app.use(). Event handlers in normal routes that do not return a value (e.g app.get()), will cause a 404 error.

If you return a value from middleware handlers, it will act as a normal event handler and sent it as response.

Example: Simple request logging middleware:

app.use((event) => {
  console.log(`[${event.req.method}] ${event.req.url}`);
  // [do not return a value]
});

You can define as much middleware as you need. They will be called in order of registration.

Read more in Guide > Routing#adding Middleware.

Converting to H3 Handler

There are situations that you might want to convert an event handler or utility made for Node.js or another framework to h3. There are built-in utils to do this.!

Converting from Web Handlers

Request handlers with Request => Response syntax can be natively converted into h3 event handlers

import { H3, fromWebHandler } from "h3";

export const app = new H3();

const webHandler = (request) => new Response("👋 Hello!"));

// Using fromWebHandler utiliy
app.get("/web", fromWebHandler(webHandler));

// Using simple wrapper
app.get("/web", event => webHandler(event.req));

Converting from Node.js Handlers

If you have a legacy request handler with (req, res) => {} syntax made for Node.js, you can use fromNodeHandler to convert it to an h3 event handler.

Node.js event handlers can only run within Node.js server runtime!
import { H3, fromNodeHandler } from "h3";

export const app = new H3();

const nodeHandler = (req, res) => {
  res.end("Node handlers work!");
};

app.get("/web", fromNodeHandler(nodeHandler));