@depup/h3 2.0.1-depup.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/bin/h3.mjs +36 -0
  4. package/changes.json +5 -0
  5. package/dist/THIRD-PARTY-LICENSES.md +70 -0
  6. package/dist/_entries/bun.d.mts +6 -0
  7. package/dist/_entries/bun.mjs +16 -0
  8. package/dist/_entries/cloudflare.d.mts +6 -0
  9. package/dist/_entries/cloudflare.mjs +16 -0
  10. package/dist/_entries/deno.d.mts +6 -0
  11. package/dist/_entries/deno.mjs +16 -0
  12. package/dist/_entries/generic.d.mts +6 -0
  13. package/dist/_entries/generic.mjs +16 -0
  14. package/dist/_entries/node.d.mts +10 -0
  15. package/dist/_entries/node.mjs +19 -0
  16. package/dist/_entries/service-worker.d.mts +6 -0
  17. package/dist/_entries/service-worker.mjs +16 -0
  18. package/dist/_utils.mjs +240 -0
  19. package/dist/cache.mjs +599 -0
  20. package/dist/cache2.mjs +50 -0
  21. package/dist/cors.mjs +292 -0
  22. package/dist/docs/0.guide/0.index/index.md +117 -0
  23. package/dist/docs/0.guide/1.basics/0.lifecycle.md +68 -0
  24. package/dist/docs/0.guide/1.basics/1.routing.md +167 -0
  25. package/dist/docs/0.guide/1.basics/2.middleware.md +97 -0
  26. package/dist/docs/0.guide/1.basics/3.handler.md +165 -0
  27. package/dist/docs/0.guide/1.basics/4.response.md +171 -0
  28. package/dist/docs/0.guide/1.basics/5.error.md +117 -0
  29. package/dist/docs/0.guide/1.basics/6.nested-apps.md +57 -0
  30. package/dist/docs/0.guide/2.rules.md +698 -0
  31. package/dist/docs/0.guide/3.api/0.h3.md +144 -0
  32. package/dist/docs/0.guide/3.api/1.h3event.md +160 -0
  33. package/dist/docs/0.guide/4.advanced/0.plugins.md +50 -0
  34. package/dist/docs/0.guide/4.advanced/1.websocket.md +176 -0
  35. package/dist/docs/0.guide/4.advanced/2.nightly.md +13 -0
  36. package/dist/docs/1.utils/0.index/index.md +46 -0
  37. package/dist/docs/1.utils/1.request.md +447 -0
  38. package/dist/docs/1.utils/2.response.md +172 -0
  39. package/dist/docs/1.utils/3.cookie.md +33 -0
  40. package/dist/docs/1.utils/4.security.md +175 -0
  41. package/dist/docs/1.utils/5.proxy.md +57 -0
  42. package/dist/docs/1.utils/6.mcp.md +75 -0
  43. package/dist/docs/1.utils/7.more.md +117 -0
  44. package/dist/docs/1.utils/8.community.md +48 -0
  45. package/dist/docs/2.examples/0.index/index.md +17 -0
  46. package/dist/docs/2.examples/1.handle-cookie.md +67 -0
  47. package/dist/docs/2.examples/2.handle-query.md +76 -0
  48. package/dist/docs/2.examples/3.handle-session.md +210 -0
  49. package/dist/docs/2.examples/4.serve-static-assets.md +66 -0
  50. package/dist/docs/2.examples/5.stream-response.md +76 -0
  51. package/dist/docs/2.examples/6.validate-data.md +193 -0
  52. package/dist/docs/3.migration/0.index/index.md +204 -0
  53. package/dist/docs/README.md +37 -0
  54. package/dist/h3.d.mts +1669 -0
  55. package/dist/h3.mjs +1809 -0
  56. package/dist/index.d.mts +1634 -0
  57. package/dist/match.d.mts +123 -0
  58. package/dist/middleware.mjs +123 -0
  59. package/dist/normalize.mjs +645 -0
  60. package/dist/path.mjs +42 -0
  61. package/dist/proxy.mjs +254 -0
  62. package/dist/response.mjs +465 -0
  63. package/dist/rules/cache.d.mts +29 -0
  64. package/dist/rules/cache.mjs +163 -0
  65. package/dist/rules/compiler.d.mts +94 -0
  66. package/dist/rules/compiler.mjs +173 -0
  67. package/dist/rules/index.d.mts +77 -0
  68. package/dist/rules/index.mjs +34 -0
  69. package/dist/rules/proxy.d.mts +3 -0
  70. package/dist/rules/proxy.mjs +14 -0
  71. package/dist/tracing.d.mts +33 -0
  72. package/dist/tracing.mjs +89 -0
  73. package/package.json +148 -0
@@ -0,0 +1,210 @@
1
+ # Sessions
2
+
3
+ > Remember your users using a session.
4
+
5
+ A session is a way to remember users using cookies. It is a very common method for authenticating users or saving data about them, such as their language or preferences on the web.
6
+
7
+ H3 provides many utilities to handle sessions:
8
+
9
+ - `useSession` initializes a session and returns a wrapper to control it.
10
+ - `getSession` retrieves the current user session, without starting one.
11
+ - `updateSession` updates the data of the current session.
12
+ - `clearSession` clears the current session.
13
+ Most of the time, you will use `useSession` to manipulate the session.
14
+
15
+ ## Initialize a Session
16
+
17
+ To initialize a session, you need to use `useSession` in an [event handler](/guide/basics/handler):
18
+
19
+ ```js
20
+ import { useSession } from "h3";
21
+
22
+ app.use(async (event) => {
23
+ const session = await useSession(event, {
24
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
25
+ });
26
+
27
+ // do something...
28
+ });
29
+ ```
30
+
31
+ > [!WARNING]
32
+ > The `password` seals every session cookie, and its **entropy is the real security boundary**. A stolen session cookie carries the salt and integrity digest in plaintext, so a weak or guessable password can be brute-forced offline — increasing PBKDF2 iterations only slows this, it does not fix a low-entropy secret. Always generate the password from a cryptographically secure random source, for example:
33
+
34
+ > ```sh
35
+ > node -e "console.log(require('node:crypto').randomBytes(32).toString('base64'))"
36
+ > ```
37
+
38
+ > The examples below use a hardcoded value so they stay readable, but in a real app load a randomly generated secret of at least 32 characters from an environment variable such as `process.env.SESSION_PASSWORD`, and never commit it to source control. A guessable passphrase (even one ≥32 characters) is not safe.
39
+
40
+ This will initialize a session and return an header `Set-Cookie` with a cookie named `h3` and an encrypted content.
41
+
42
+ If the request contains a cookie named `h3` or a header named `x-h3-session`, the session will be initialized with the content of the cookie or the header.
43
+
44
+ > [!NOTE]
45
+ > The header take precedence over the cookie.
46
+
47
+ ## Get Data from a Session
48
+
49
+ To get data from a session, we will still use `useSession`. Under the hood, it will use `getSession` to get the session.
50
+
51
+ ```js
52
+ import { useSession } from "h3";
53
+
54
+ app.use(async (event) => {
55
+ const session = await useSession(event, {
56
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
57
+ });
58
+
59
+ return session.data;
60
+ });
61
+ ```
62
+
63
+ Data are stored in the `data` property of the session. If there is no data, it will be an empty object.
64
+
65
+ ## Add Data to a Session
66
+
67
+ To add data to a session, we will still use `useSession`. Under the hood, it will use `updateSession` to update the session.
68
+
69
+ ```js
70
+ import { useSession } from "h3";
71
+
72
+ app.use(async (event) => {
73
+ const session = await useSession(event, {
74
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
75
+ });
76
+
77
+ const count = (session.data.count || 0) + 1;
78
+ await session.update({
79
+ count: count,
80
+ });
81
+
82
+ return count === 0 ? "Hello world!" : `Hello world! You have visited this page ${count} times.`;
83
+ });
84
+ ```
85
+
86
+ What is happening here?
87
+
88
+ We try to get a session from the request. If there is no session, a new one will be created. Then, we increment the `count` property of the session and we update the session with the new value. Finally, we return a message with the number of times the user visited the page.
89
+
90
+ Try to visit the page multiple times and you will see the number of times you visited the page.
91
+
92
+ > [!NOTE]
93
+ > If you use a CLI tool like `curl` to test this example, you will not see the number of times you visited the page because the CLI tool does not save cookies. You must get the cookie from the response and send it back to the server.
94
+
95
+ ## Clear a Session
96
+
97
+ To clear a session, we will still use `useSession`. Under the hood, it will use `clearSession` to clear the session.
98
+
99
+ ```js
100
+ import { useSession } from "h3";
101
+
102
+ app.use("/clear", async (event) => {
103
+ const session = await useSession(event, {
104
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
105
+ });
106
+
107
+ await session.clear();
108
+
109
+ return "Session cleared";
110
+ });
111
+ ```
112
+
113
+ H3 will send a header `Set-Cookie` with an empty cookie named `h3` to clear the session.
114
+
115
+ ## Options
116
+
117
+ When to use `useSession`, you can pass an object with options as the second argument to configure the session:
118
+
119
+ ```js
120
+ import { useSession } from "h3";
121
+
122
+ app.use(async (event) => {
123
+ const session = await useSession(event, {
124
+ name: "my-session",
125
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
126
+ cookie: {
127
+ httpOnly: true,
128
+ secure: true,
129
+ sameSite: "strict",
130
+ },
131
+ maxAge: 60 * 60 * 24 * 7, // 7 days
132
+ });
133
+
134
+ return session.data;
135
+ });
136
+ ```
137
+
138
+ Every option is optional except `password`. The `name` option is worth calling out: it sets the cookie used to store the session and defaults to `h3`. H3 also reads the session from a request header derived from `name`, which it normalizes to lowercase as `x-${name.toLowerCase()}-session`, so the default name `h3` produces the `x-h3-session` header seen earlier. A mixed-case `name` like `MyApp` still resolves to a lowercase `x-myapp-session` header, while the cookie keeps the original casing. That default is why the earlier examples set a cookie named `h3`.
139
+
140
+ > [!NOTE]
141
+ > The session cookie defaults to `secure: true`, `httpOnly: true`, `sameSite: "lax"`, and `path: "/"`. Any of these can be overridden via `cookie`.
142
+
143
+ > [!NOTE]
144
+ > The `secure: true` option tells the browser to only store and send the cookie over HTTPS. When developing locally over plain HTTP, compliant browsers (notably Safari and iOS, and Chrome on some local domains) silently drop the cookie, so the session will not persist. Set `cookie: { secure: false }` during local development to work around this.
145
+
146
+ ## Expiration
147
+
148
+ Sessions have two independent expiration controls, and you can use either or both:
149
+
150
+ - `maxAge` is an **absolute** lifetime, counted from when the session was created. It is reached however active the user is.
151
+ - `idleTimeout` is a **sliding** lifetime, counted from the last request. An active user stays signed in; an idle one is signed out.
152
+
153
+ ```js
154
+ const session = await useSession(event, {
155
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
156
+ idleTimeout: 60 * 30, // signed out after 30 minutes of inactivity...
157
+ maxAge: 60 * 60 * 24 * 7, // ...and after 7 days regardless
158
+ });
159
+ ```
160
+ With `idleTimeout` set, H3 moves the idle window forward by resealing the session cookie with the reseal time stamped into it. `createdAt` is left untouched, which is what lets `maxAge` still act as a hard cap on top. The cookie `Expires` is set to whichever limit runs out first.
161
+
162
+ If you are coming from `express-session` or `koa-session`, `idleTimeout` is their `rolling` option. The difference is that it carries its own duration instead of reinterpreting `maxAge`, so enabling it does not cost you the absolute limit.
163
+
164
+ Resealing is the expensive part of a session, so H3 does not do it on every request: it reseals only once more than half the window has been used, and updating the session counts as a reseal. An active user therefore never gets signed out, but the recorded last-seen time can trail the real one by up to half the window:
165
+
166
+ ```js
167
+ // idleTimeout: 60 * 30
168
+ // Sign-out happens 15 to 30 minutes after the last request, never later.
169
+ ```
170
+
171
+ Halve `idleTimeout` if you need the shorter end of that range to be your real limit.
172
+
173
+ > [!NOTE]
174
+ > Only cookie sessions slide. A session sent through the `x-{name}-session` header cannot be resealed, so it expires `idleTimeout` after its seal was issued.
175
+
176
+ > [!IMPORTANT]
177
+ > Because the session lives in the cookie, a request that only reads the session writes it back when it slides the window. If such a request overlaps with one that writes the session, whichever response the browser applies last wins, so the write can be lost. Without `idleTimeout` a read-only request sets no cookie and cannot clobber a concurrent write.
178
+
179
+ > [!NOTE]
180
+ > A request that slides the window pays for an extra seal and puts a `Set-Cookie` header on its response — shared caches and CDNs often refuse to store those. Requests that only read the session inside the throttle window set no cookie at all.
181
+
182
+ The session cookie is also applied to error responses, so a request that throws still slides the window and still persists a session created during it.
183
+
184
+ ## Use Multiple Sessions
185
+
186
+ Because each session is stored under its own `name`, you can run several independent sessions on the same request. They live in separate cookies and never overwrite each other, which is useful for keeping unrelated concerns apart, such as a long-lived auth session and a short-lived flash message:
187
+
188
+ ```js
189
+ import { useSession } from "h3";
190
+
191
+ app.use(async (event) => {
192
+ const auth = await useSession(event, {
193
+ name: "auth",
194
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
195
+ });
196
+
197
+ const flash = await useSession(event, {
198
+ name: "flash",
199
+ password: "80d42cfb-1cd2-462c-8f17-e3237d9027e9",
200
+ });
201
+
202
+ await flash.update({ message: "Saved!" });
203
+
204
+ // `auth` and `flash` are backed by different cookies, so they stay separate
205
+ return { user: auth.data.user, flash: flash.data.message };
206
+ });
207
+ ```
208
+
209
+ > [!NOTE]
210
+ > Give each session a distinct `name`. Two sessions that share a name share the same cookie, so the last write wins.
@@ -0,0 +1,66 @@
1
+ # Static Assets
2
+
3
+ > Serve static assets such as HTML, images, CSS, JavaScript, etc.
4
+
5
+ H3 can serve static assets such as HTML, images, CSS, JavaScript, etc.
6
+
7
+ To serve a static directory, you can use the `serveStatic` utility.
8
+
9
+ ```ts
10
+ import { H3, serveStatic } from "h3";
11
+
12
+ const app = new H3();
13
+
14
+ app.use("/public/**", (event) => {
15
+ return serveStatic(event, {
16
+ getContents: (id) => {
17
+ // TODO
18
+ },
19
+ getMeta: (id) => {
20
+ // TODO
21
+ },
22
+ });
23
+ });
24
+ ```
25
+
26
+ This does not serve any files yet. You need to implement the `getContents` and `getMeta` methods.
27
+
28
+ - `getContents` is used to read the contents of a file. It should return a `Promise` that resolves to the contents of the file or `undefined` if the file does not exist.
29
+ - `getMeta` is used to get the metadata of a file. It should return a `Promise` that resolves to the metadata of the file or `undefined` if the file does not exist.
30
+ They are separated to allow H3 to respond to `HEAD` requests without reading the contents of the file and to use the `Last-Modified` header.
31
+
32
+ ## Read files
33
+
34
+ Now, create a `index.html` file in the `public` directory with a simple message and open your browser to http://localhost:3000. You should see the message.
35
+
36
+ Then, we can create the `getContents` and `getMeta` methods:
37
+
38
+ ```ts
39
+ import { stat, readFile } from "node:fs/promises";
40
+ import { join } from "node:path";
41
+ import { H3, serve, serveStatic } from "h3";
42
+
43
+ const app = new H3();
44
+
45
+ app.use("/public/**", (event) => {
46
+ return serveStatic(event, {
47
+ indexNames: ["/index.html"],
48
+ getContents: (id) => readFile(join("public", id)),
49
+ getMeta: async (id) => {
50
+ const stats = await stat(join("public", id)).catch(() => {});
51
+ if (stats?.isFile()) {
52
+ return {
53
+ size: stats.size,
54
+ mtime: stats.mtimeMs,
55
+ };
56
+ }
57
+ },
58
+ });
59
+ });
60
+
61
+ serve(app);
62
+ ```
63
+
64
+ The `getContents` reads the file and returns its contents, pretty simple. The `getMeta` uses `fs.stat` to get the file metadata. If the file does not exist or is not a file, it returns `undefined`. Otherwise, it returns the file size and the last modification time.
65
+
66
+ The file size and last modification time are used to create an etag to send a `304 Not Modified` response if the file has not been modified since the last request. This is useful to avoid sending the same file multiple times if it has not changed.
@@ -0,0 +1,76 @@
1
+ # Stream Response
2
+
3
+ > Stream response to the client.
4
+
5
+ Using stream responses It allows you to send data to the client as soon as you have it. This is useful for large files or long running responses.
6
+
7
+ ## Create a Stream
8
+
9
+ To stream a response, you first need to create a stream using the [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) API:
10
+
11
+ ```ts
12
+ const stream = new ReadableStream();
13
+ ```
14
+
15
+ For the example, we will create a start function that will send a random number every 100 milliseconds. After 1000 milliseconds, it will close the stream:
16
+
17
+ ```ts
18
+ let interval: NodeJS.Timeout;
19
+ const stream = new ReadableStream({
20
+ start(controller) {
21
+ controller.enqueue("<ul>");
22
+
23
+ interval = setInterval(() => {
24
+ controller.enqueue("<li>" + Math.random() + "</li>");
25
+ }, 100);
26
+
27
+ setTimeout(() => {
28
+ clearInterval(interval);
29
+ controller.close();
30
+ }, 1000);
31
+ },
32
+ cancel() {
33
+ clearInterval(interval);
34
+ },
35
+ });
36
+ ```
37
+
38
+ ## Send a Stream
39
+
40
+ ```ts
41
+ import { H3 } from "h3";
42
+
43
+ export const app = new H3();
44
+
45
+ app.use((event) => {
46
+ // Set to response header to tell to the client that we are sending a stream.
47
+ event.res.headers.set("Content-Type", "text/html");
48
+ event.res.headers.set("Cache-Control", "no-cache");
49
+ event.res.headers.set("Transfer-Encoding", "chunked");
50
+
51
+ let interval: NodeJS.Timeout;
52
+ const stream = new ReadableStream({
53
+ start(controller) {
54
+ controller.enqueue("<ul>");
55
+
56
+ interval = setInterval(() => {
57
+ controller.enqueue("<li>" + Math.random() + "</li>");
58
+ }, 100);
59
+
60
+ setTimeout(() => {
61
+ clearInterval(interval);
62
+ controller.close();
63
+ }, 1000);
64
+ },
65
+ cancel() {
66
+ clearInterval(interval);
67
+ },
68
+ });
69
+
70
+ return stream;
71
+ });
72
+ ```
73
+
74
+ Open your browser to http://localhost:3000 and you should see a list of random numbers appearing every 100 milliseconds.
75
+
76
+ Magic! 🎉
@@ -0,0 +1,193 @@
1
+ # Validate Data
2
+
3
+ > Ensure that your data are valid and safe before processing them.
4
+
5
+ When you receive data on your server, you must validate them. By validate, we mean that the shape of the received data must match the expected shape. It's important because you can't trust the data coming from unknown sources, like a user or an external API.
6
+
7
+ > [!WARNING]
8
+ > Do not use type generics as a validation. Providing an interface to a utility like `readBody` is not a validation. You must validate the data before using it.
9
+
10
+ ## Utilities for Validation
11
+
12
+ H3 provide some utilities to help you to handle data validation. You will be able to validate:
13
+
14
+ - query with `getValidatedQuery`
15
+ - params with `getValidatedRouterParams`.
16
+ - body with `readValidatedBody`
17
+ H3 doesn't provide any validation library but it does support schemas coming from a **Standard-Schema** compatible one, like: [Zod](https://zod.dev), [Valibot](https://valibot.dev), [ArkType](https://arktype.io/), etc... (for all compatible libraries please check [their official repository](https://github.com/standard-schema/standard-schema)). If you want to use a validation library that is not compatible with Standard-Schema, you can still use it, but you will have to use parsing functions provided by the library itself (refer to the [Safe Parsing](#safe-parsing) section below).
18
+
19
+ > [!WARNING]
20
+ > H3 is runtime agnostic. This means that you can use it in [any runtime](/guide). But some validation libraries are not compatible with all runtimes.
21
+
22
+ Let's see how to validate data with [Zod](https://zod.dev) and [Valibot](https://valibot.dev).
23
+
24
+ ### Validate Params
25
+
26
+ You can use `getValidatedRouterParams` to validate params and get the result, as a replacement of `getRouterParams`:
27
+
28
+ ```js
29
+ import { getValidatedRouterParams } from "h3";
30
+ import * as z from "zod";
31
+ import * as v from "valibot";
32
+
33
+ // Example with Zod
34
+ const contentSchema = z.object({
35
+ topic: z.string().min(1),
36
+ uuid: z.string().uuid(),
37
+ });
38
+ // Example with Valibot
39
+ const contentSchema = v.object({
40
+ topic: v.pipe(v.string(), v.nonEmpty()),
41
+ uuid: v.pipe(v.string(), v.uuid()),
42
+ });
43
+
44
+ app.all(
45
+ // You must use a router to use params
46
+ "/content/:topic/:uuid",
47
+ async (event) => {
48
+ const params = await getValidatedRouterParams(event, contentSchema);
49
+ return `You are looking for content with topic "${params.topic}" and uuid "${params.uuid}".`;
50
+ },
51
+ );
52
+ ```
53
+
54
+ If you send a valid request like `/content/posts/123e4567-e89b-12d3-a456-426614174000` to this event handler, you will get a response like this:
55
+
56
+ ```txt
57
+ You are looking for content with topic "posts" and uuid "123e4567-e89b-12d3-a456-426614174000".
58
+ ```
59
+
60
+ If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
61
+
62
+ ### Validate Query
63
+
64
+ You can use `getValidatedQuery` to validate query and get the result, as a replacement of `getQuery`:
65
+
66
+ ```js
67
+ import { getValidatedQuery } from "h3";
68
+ import * as z from "zod";
69
+ import * as v from "valibot";
70
+
71
+ // Example with Zod
72
+ const stringToNumber = z.string().regex(/^\d+$/, "Must be a number string").transform(Number);
73
+ const paginationSchema = z.object({
74
+ page: stringToNumber.optional().default(1),
75
+ size: stringToNumber.optional().default(10),
76
+ });
77
+
78
+ // Example with Valibot
79
+ const stringToNumber = v.pipe(
80
+ v.string(),
81
+ v.regex(/^\d+$/, "Must be a number string"),
82
+ v.transform(Number),
83
+ );
84
+ const paginationSchema = v.object({
85
+ page: v.optional(stringToNumber, 1),
86
+ size: v.optional(stringToNumber, 10),
87
+ });
88
+
89
+ app.use(async (event) => {
90
+ const query = await getValidatedQuery(event, paginationSchema);
91
+ return `You are on page ${query.page} with ${query.size} items per page.`;
92
+ });
93
+ ```
94
+
95
+ As you may have noticed, compared to the `getValidatedRouterParams` example, we can leverage validation libraries to transform the incoming data. In this case, we transform the string representation of a number into a real number, which is useful for things like content pagination.
96
+
97
+ If you send a valid request like `/?page=2&size=20` to this event handler, you will get a response like this:
98
+
99
+ ```txt
100
+ You are on page 2 with 20 items per page.
101
+ ```
102
+
103
+ If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
104
+
105
+ ### Validate Body
106
+
107
+ You can use `readValidatedBody` to validate body and get the result, as a replacement of `readBody`:
108
+
109
+ ```js
110
+ import { readValidatedBody } from "h3";
111
+ import { z } from "zod";
112
+ import * as v from "valibot";
113
+
114
+ // Example with Zod
115
+ const userSchema = z.object({
116
+ name: z.string().min(3).max(20),
117
+ age: z.number({ coerce: true }).positive().int(),
118
+ });
119
+
120
+ // Example with Valibot
121
+ const userSchema = v.object({
122
+ name: v.pipe(v.string(), v.minLength(3), v.maxLength(20)),
123
+ age: v.pipe(v.number(), v.integer(), v.minValue(0)),
124
+ });
125
+
126
+ app.use(async (event) => {
127
+ const body = await readValidatedBody(event, userSchema);
128
+ return `Hello ${body.name}! You are ${body.age} years old.`;
129
+ });
130
+ ```
131
+
132
+ If you send a valid POST request with a JSON body like this:
133
+
134
+ ```json
135
+ {
136
+ "name": "John",
137
+ "age": 42
138
+ }
139
+ ```
140
+
141
+ You will get a response like this:
142
+
143
+ ```txt
144
+ Hello John! You are 42 years old.
145
+ ```
146
+
147
+ If you send an invalid request and the validation fails, H3 will throw a `400 Validation Error` error. In the data of the error, you will find the validation errors you can use on your client to display a nice error message to your user.
148
+
149
+ ## Safe Parsing
150
+
151
+ By default if a schema is directly provided as e second argument for each validation utility (`getValidatedRouterParams`, `getValidatedQuery`, and `readValidatedBody`) it will throw a `400 Validation Error` error if the validation fails, but in some cases you may want to handle the validation errors yourself. For this you should provide the actual safe validation function as the second argument, depending on the validation library you are using.
152
+
153
+ Going back to the first example with `getValidatedRouterParams`, for Zod it would look like this:
154
+
155
+ ```ts
156
+ import { getValidatedRouterParams } from "h3";
157
+ import { z } from "zod/v4";
158
+
159
+ const contentSchema = z.object({
160
+ topic: z.string().min(1),
161
+ uuid: z.string().uuid(),
162
+ });
163
+
164
+ app.all("/content/:topic/:uuid", async (event) => {
165
+ const params = await getValidatedRouterParams(event, contentSchema.safeParse);
166
+ if (!params.success) {
167
+ // Handle validation errors
168
+ return `Validation failed:\n${z.prettifyError(params.error)}`;
169
+ }
170
+ return `You are looking for content with topic "${params.data.topic}" and uuid "${params.data.uuid}".`;
171
+ });
172
+ ```
173
+
174
+ And for Valibot, it would look like this:
175
+
176
+ ```ts
177
+ import { getValidatedRouterParams } from "h3";
178
+ import * as v from "valibot";
179
+
180
+ const contentSchema = v.object({
181
+ topic: v.pipe(v.string(), v.nonEmpty()),
182
+ uuid: v.pipe(v.string(), v.uuid()),
183
+ });
184
+
185
+ app.all("/content/:topic/:uuid", async (event) => {
186
+ const params = await getValidatedRouterParams(event, v.safeParser(contentSchema));
187
+ if (!params.success) {
188
+ // Handle validation errors
189
+ return `Validation failed:\n${v.summarize(params.issues)}`;
190
+ }
191
+ return `You are looking for content with topic "${params.output.topic}" and uuid "${params.output.uuid}".`;
192
+ });
193
+ ```