diff --git a/README.md b/README.md
index 3e8e60ae..822d9095 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,9 @@
H3 (pronounced as /eษชtสฮธriห/, like h-3) is a minimal h(ttp) framework built for high performance and portability.
-๐ [Online Playground](https://stackblitz.com/github/unjs/h3/tree/main/playground?startScript=dev)
+๐ [Online Playground](https://stackblitz.com/github/unjs/h3/tree/main/playground)
+
+๐ [Online Examples Playground](https://stackblitz.com/github/unjs/h3/tree/main/examples)
## Features
@@ -135,7 +137,7 @@ app.use(router);
Routes are internally stored in a [Radix Tree](https://en.wikipedia.org/wiki/Radix_tree) and matched using [unjs/radix3](https://github.com/unjs/radix3).
-For using nested routers, see [this example](https://stackblitz.com/edit/github-2bmusk?file=app.ts&startScript=dev)
+For using nested routers, see [this example](./examples/nested-router.ts)
## More app usage examples
diff --git a/examples/body.ts b/examples/body.ts
new file mode 100644
index 00000000..9d585a6d
--- /dev/null
+++ b/examples/body.ts
@@ -0,0 +1,23 @@
+import { createApp, createRouter, defineEventHandler, readBody } from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/",
+ defineEventHandler(() => {
+ return "use POST method to try!";
+ }),
+ )
+ .post(
+ "/",
+ defineEventHandler(async (event) => {
+ const body = await readBody(event);
+ // Use can also use `readFormData` to get a FormData object, `readMultiPartFormData` to get an array of MultiPartData or `readRawBody` to get a Buffer.
+ return {
+ body,
+ };
+ }),
+ );
+
+app.use(router);
diff --git a/examples/cookies.ts b/examples/cookies.ts
new file mode 100644
index 00000000..3bd96ee8
--- /dev/null
+++ b/examples/cookies.ts
@@ -0,0 +1,31 @@
+import {
+ createApp,
+ createRouter,
+ defineEventHandler,
+ getCookie,
+ setCookie,
+} from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/",
+ defineEventHandler((event) => {
+ const testCookie = getCookie(event, "testCookie");
+ return `testCookie is ${JSON.stringify(
+ testCookie,
+ )} (go to /set to set it)`;
+ }),
+ )
+ .get(
+ "/set",
+ defineEventHandler((event) => {
+ // By default, path is set to `/`. You can use any of the options supported by the Set-Cookie header.
+ // @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
+ setCookie(event, "testCookie", "bar", { httpOnly: true });
+ return "testCookie is set";
+ }),
+ );
+
+app.use(router);
diff --git a/examples/errors.ts b/examples/errors.ts
new file mode 100644
index 00000000..a330bc39
--- /dev/null
+++ b/examples/errors.ts
@@ -0,0 +1,37 @@
+import { createApp, createError, createRouter, defineEventHandler } from "h3";
+
+export const app = createApp({ debug: true });
+
+const router = createRouter()
+ .get(
+ "/",
+ defineEventHandler(() => {
+ // Always "throw" errors to propgate them to the error handler
+ throw createError({ statusMessage: "Simple error!", statusCode: 301 });
+ }),
+ )
+ .get(
+ "/complexe-error",
+ defineEventHandler(() => {
+ // You can fully customize errors by adding data, cause and if it's a fatal error or not
+ throw createError({
+ status: 400,
+ message: "Bad request",
+ statusMessage: "Bad request message",
+ });
+ }),
+ )
+ .get(
+ "/fatal-error",
+ defineEventHandler(() => {
+ // Fatal errors will stop the execution of the current request and will be logged
+ throw createError({
+ status: 500,
+ message: "Fatal error",
+ fatal: true,
+ data: { foo: "bar" },
+ });
+ }),
+ );
+
+app.use(router);
diff --git a/examples/first-server.ts b/examples/first-server.ts
new file mode 100644
index 00000000..3c108f79
--- /dev/null
+++ b/examples/first-server.ts
@@ -0,0 +1,48 @@
+import { createApp, defineEventHandler, toNodeListener } from "h3";
+
+export const app = createApp();
+
+app
+ // `/` is the root path and will response to every request.
+ .use(
+ "/first-request",
+ defineEventHandler(() => {
+ return "hello world";
+ }),
+ )
+ .use(
+ "/hello",
+ defineEventHandler(() => {
+ return "world";
+ }),
+ )
+ .use(
+ "/json",
+ defineEventHandler(() => {
+ // Automatically set the `Content-Type` header to `application/json`.
+ return {
+ hello: "world",
+ };
+ }),
+ )
+ .use(
+ "/html",
+ defineEventHandler(() => {
+ // By default, the `Content-Type` header is set to `text/html`.
+ return "
hello world
";
+ }),
+ )
+ .use(
+ "/buffer",
+ defineEventHandler(() => {
+ // No `Content-Type` header is set by default. You can set it manually using `setHeader`.
+ return Buffer.from("hello world");
+ }),
+ )
+ .use(
+ "/blob",
+ defineEventHandler(() => {
+ // No `Content-Type` header is set by default. You can set it manually using `setHeader`.
+ return new Blob(["hello world"]);
+ }),
+ );
diff --git a/examples/handler-middleware.ts b/examples/handler-middleware.ts
new file mode 100644
index 00000000..375e80a2
--- /dev/null
+++ b/examples/handler-middleware.ts
@@ -0,0 +1,30 @@
+import {
+ createApp,
+ createRouter,
+ defineEventHandler,
+ defineRequestMiddleware,
+ defineResponseMiddleware,
+} from "h3";
+
+export const app = createApp();
+
+const router = createRouter().get(
+ "/",
+ defineEventHandler({
+ onRequest: defineRequestMiddleware(() => {
+ // Do anything you want here like authentication, rate limiting, etc.
+ console.log("onRequest");
+ // Never return anything from onRequest to avoid to close the connection
+ }),
+ onBeforeResponse: defineResponseMiddleware(() => {
+ // Do anything you want here like logging, collecting metrics, or output compression, etc.
+ console.log("onResponse");
+ // Never return anything from onResponse to avoid to close the connection
+ }),
+ handler: defineEventHandler(() => {
+ return "GET: hello world";
+ }),
+ }),
+);
+
+app.use(router);
diff --git a/examples/headers.ts b/examples/headers.ts
new file mode 100644
index 00000000..17ff427f
--- /dev/null
+++ b/examples/headers.ts
@@ -0,0 +1,35 @@
+import {
+ createApp,
+ createRouter,
+ defineEventHandler,
+ getRequestHeader,
+ getResponseHeaders,
+ setResponseHeader,
+} from "h3";
+
+export const app = createApp();
+
+const router = createRouter().get(
+ "/user-agent",
+ defineEventHandler((event) => {
+ const userAgent = getRequestHeader(event, "user-agent");
+ // You can also use `getRequestHeaders` to get all headers at once.
+ // const headers = getRequestHeaders(event)
+
+ setResponseHeader(event, "content-type", "text/plain");
+ setResponseHeader(event, "x-server", "nitro");
+ // You can also use `setResponseHeaders` to set multiple headers at once.
+ // setResponseHeaders(event, { 'x-server': 'nitro', 'content-type': 'text/plain' })
+
+ const responseHeaders = getResponseHeaders(event);
+ // You can also use `getResponseHeader` to get a single header.
+ // const contentType = getResponseHeader(event, 'content-type')
+
+ return {
+ userAgent,
+ responseHeaders,
+ };
+ }),
+);
+
+app.use(router);
diff --git a/examples/index.mjs b/examples/index.mjs
new file mode 100644
index 00000000..53c450d2
--- /dev/null
+++ b/examples/index.mjs
@@ -0,0 +1,19 @@
+import { readdir } from "node:fs/promises";
+import { listenAndWatch } from "listhen";
+
+async function promptExample() {
+ const { consola } = await import("consola");
+ const exampleFiles = await readdir(new URL(".", import.meta.url)).then((r) =>
+ r.filter((f) => f.endsWith(".ts")),
+ );
+ return await consola.prompt("Select an example to run:", {
+ type: "select",
+ options: exampleFiles,
+ });
+}
+
+const exampleFile = process.argv[2] || (await promptExample());
+
+listenAndWatch(new URL(exampleFile, import.meta.url), {
+ name: `H3 example: ${exampleFile}`,
+});
diff --git a/examples/nested-router.ts b/examples/nested-router.ts
new file mode 100644
index 00000000..5dd59347
--- /dev/null
+++ b/examples/nested-router.ts
@@ -0,0 +1,27 @@
+import {
+ createApp,
+ defineEventHandler,
+ toNodeListener,
+ createRouter,
+ useBase,
+ sendRedirect,
+} from "h3";
+
+// Init App
+export const app = createApp({ debug: true });
+
+// Main Router
+const router = createRouter();
+router.use(
+ "/",
+ defineEventHandler((event) => sendRedirect(event, "/api/test")),
+);
+app.use(router);
+
+// Nested API Ruter
+const apiRouter = createRouter();
+router.use("/api/**", useBase("/api", apiRouter.handler));
+apiRouter.use(
+ "/test",
+ defineEventHandler(() => "API /test"),
+);
diff --git a/examples/package.json b/examples/package.json
new file mode 100644
index 00000000..01165df4
--- /dev/null
+++ b/examples/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "h3-examples",
+ "private": true,
+ "scripts": {
+ "dev": "node ./index.mjs"
+ },
+ "dependencies": {
+ "h3": "latest",
+ "listhen": "latest",
+ "consola": "latest"
+ }
+}
diff --git a/examples/query-params.ts b/examples/query-params.ts
new file mode 100644
index 00000000..6eaa772a
--- /dev/null
+++ b/examples/query-params.ts
@@ -0,0 +1,18 @@
+import { createApp, createRouter, defineEventHandler, getQuery } from "h3";
+
+export const app = createApp();
+
+const router = createRouter().get(
+ "/",
+ defineEventHandler((event) => {
+ const query = getQuery(event);
+
+ if (!query.name) {
+ return "Set ?name=yourname in URL to get a greeting!";
+ }
+
+ return `Hello ${query.name}`;
+ }),
+);
+
+app.use(router);
diff --git a/examples/redirect.ts b/examples/redirect.ts
new file mode 100644
index 00000000..1e8dbaa5
--- /dev/null
+++ b/examples/redirect.ts
@@ -0,0 +1,20 @@
+import { createApp, createRouter, defineEventHandler, sendRedirect } from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/unjs",
+ defineEventHandler((event) => {
+ return sendRedirect(event, "https://unjs.io/packages/h3"); // 302 Found by default
+ }),
+ )
+ .get(
+ "/permanent",
+ defineEventHandler((event) => {
+ // You can use any 3xx status code you want
+ return sendRedirect(event, "https://unjs.io/packages/h3", 301);
+ }),
+ );
+
+app.use(router);
diff --git a/examples/router.ts b/examples/router.ts
new file mode 100644
index 00000000..c049fe72
--- /dev/null
+++ b/examples/router.ts
@@ -0,0 +1,43 @@
+import { createApp, createRouter, defineEventHandler } from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/",
+ defineEventHandler(() => {
+ return "GET: hello world";
+ }),
+ )
+ .post(
+ "/",
+ defineEventHandler(() => {
+ return "POST: hello world";
+ }),
+ )
+ .put(
+ "/",
+ defineEventHandler(() => {
+ return "PUT: hello world";
+ }),
+ )
+ .delete(
+ "/",
+ defineEventHandler(() => {
+ return "DELETE: hello world";
+ }),
+ )
+ .patch(
+ "/",
+ defineEventHandler(() => {
+ return "PATCH: hello world";
+ }),
+ )
+ .head(
+ "/",
+ defineEventHandler(() => {
+ return "HEAD: hello world";
+ }),
+ );
+
+app.use(router);
diff --git a/examples/status.ts b/examples/status.ts
new file mode 100644
index 00000000..b918bbec
--- /dev/null
+++ b/examples/status.ts
@@ -0,0 +1,43 @@
+import {
+ createApp,
+ createRouter,
+ defineEventHandler,
+ getResponseStatus,
+ getResponseStatusText,
+ sendNoContent,
+ setResponseStatus,
+} from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/not-found",
+ defineEventHandler((event) => {
+ setResponseStatus(event, 404);
+
+ return "Not found"; // You need to explicitly return something to avoid a 404 'Cannot find any path matching "/not-found"' response.
+ }),
+ )
+ .get(
+ "/bad-request",
+ defineEventHandler((event) => {
+ setResponseStatus(event, 400, "Bad request message"); // You can customize the status message.
+
+ const status = getResponseStatus(event); // You can get the status message.
+ const text = getResponseStatusText(event); // You can get the status message.
+
+ return {
+ status,
+ text,
+ };
+ }),
+ )
+ .get(
+ "/no-content",
+ defineEventHandler((event) => {
+ sendNoContent(event); // Do not need to explicitly return because `sendNoContent` will cut the connection.
+ }),
+ );
+
+app.use(router);
diff --git a/examples/url-params.ts b/examples/url-params.ts
new file mode 100644
index 00000000..382581ac
--- /dev/null
+++ b/examples/url-params.ts
@@ -0,0 +1,28 @@
+import {
+ createApp,
+ createRouter,
+ defineEventHandler,
+ getRouterParam,
+ getRouterParams,
+} from "h3";
+
+export const app = createApp();
+
+const router = createRouter()
+ .get(
+ "/:name",
+ defineEventHandler((event) => {
+ const name = getRouterParam(event, "name");
+ return `Hello ${name}`;
+ }),
+ )
+ .get(
+ "/:name/:age",
+ defineEventHandler((event) => {
+ const params = getRouterParams(event);
+
+ return `Hello ${params.name}, you are ${params.age} years old`;
+ }),
+ );
+
+app.use(router);
diff --git a/package.json b/package.json
index 6ed6114f..b1c35e75 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,8 @@
"scripts": {
"build": "unbuild",
"dev": "vitest",
- "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground",
- "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground -w",
+ "lint": "eslint --cache --ext .ts,.js,.mjs,.cjs . && prettier -c src test playground examples",
+ "lint:fix": "eslint --cache --ext .ts,.js,.mjs,.cjs . --fix && prettier -c src test playground examples -w",
"play": "listhen -w ./playground/app.ts",
"profile": "0x -o -D .profile -P 'autocannon -c 100 -p 10 -d 40 http://localhost:$PORT' ./playground/server.cjs",
"release": "pnpm test && pnpm build && changelogen --release && pnpm publish && git push --follow-tags",
diff --git a/playground/package.json b/playground/package.json
index f7582346..c37d0d9a 100644
--- a/playground/package.json
+++ b/playground/package.json
@@ -6,7 +6,7 @@
"dev": "listhen -w ./app.ts"
},
"dependencies": {
- "h3": "^1.9.0",
- "listhen": "^1.5.5"
+ "h3": "latest",
+ "listhen": "latest"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bdc131be..237f0aa5 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -103,13 +103,25 @@ importers:
specifier: ^3.22.4
version: 3.22.4
+ examples:
+ dependencies:
+ consola:
+ specifier: latest
+ version: 3.2.3
+ h3:
+ specifier: latest
+ version: link:..
+ listhen:
+ specifier: latest
+ version: 1.5.5
+
playground:
dependencies:
h3:
- specifier: ^1.9.0
+ specifier: latest
version: link:..
listhen:
- specifier: ^1.5.5
+ specifier: latest
version: 1.5.5
packages:
@@ -772,6 +784,7 @@ packages:
dependencies:
is-glob: 4.0.3
micromatch: 4.0.5
+ napi-wasm: 1.1.0
bundledDependencies:
- napi-wasm
@@ -4891,6 +4904,9 @@ packages:
hasBin: true
dev: true
+ /napi-wasm@1.1.0:
+ resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==}
+
/natural-compare-lite@1.4.0:
resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==}
dev: true
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index f1ccb742..001631ac 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -1,2 +1,3 @@
packages:
- "playground"
+ - "examples"