From 7a8c8b1b8afd2351bfb4014bc47316bcb0cf4308 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Wed, 13 Sep 2023 01:16:05 +0200 Subject: [PATCH 1/8] content: add h3 101 --- .../1.learn/2.h3-101-first-hand.md | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 content/4.resources/1.learn/2.h3-101-first-hand.md diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md new file mode 100644 index 00000000..6e4521b0 --- /dev/null +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -0,0 +1,254 @@ +--- +title: h3 101 - first hand +description: Discover h3, a better API for fetch that works on Node.js, browser, and workers. +authors: + - name: Estéban Soubiran + picture: https://esteban-soubiran.site/esteban.webp + twitter: soubiran_ +packages: + - h3 +publishedAt: 2023-09-12 +modifiedAt: 2023-09-12 +layout: learn-post +--- + + + +## Installation + +First, let's create a new project: + +```bash +mkdir h3-101 +cd h3-101 +npm init -y +``` + +Then, install h3: + +```bash +npm install h3 +``` + +::alert{type="info"} +We can use the package manager of our choice like `npm`, `yarn`, `pnpm` or `bun`. +:: + +## First Server + +Before deep diving into [`unjs/h3`](https://github.com/unjs/h3), let's create the simpler server possible to understand how it works and some of its specificities. + +To do so, we can create our first file named `first-server.mjs`. + +First of all, we will first create our app, where request are processed. + +```js [first-server.mjs] +import { createApp } from 'h3' + +const app = createApp() +``` + +Then, we will plug a event handler. It's a function that will be invoked on requests. In our case, we will response to every request with `Hello World!`. + +```js [first-server.mjs] +import { createApp, defineEventHandler } from 'h3' + +const app = createApp() + .use('/', defineEventHandler(() => { + return new Response('Hello World!') + })) +``` + +But our app does not listen requests, just handle them for the moment. To listen for requests to be handled, we will use [`node:http`](https://nodejs.org/api/http.html) to create a server and listen on port `3000`. + +```js [first-server.mjs] +import { createServer } from 'node:http' +import { createApp, defineEventHandler, toNodeListener } from 'h3' + +const app = createApp() + .use('/', defineEventHandler(() => { + return new Response('Hello World!') + })) + +createServer(toNodeListener(app)).listen(3000) +``` + +Finally, we can run it with [`Node.js`](https://nodejs.org/en/): + +```bash +node first-script.mjs +``` + +Then, we can use [`curl`](https://curl.se/) to test it: + +```bash +curl http://localhost:3000 +# Output: Hello World! +``` + +And it works! + +### Understanding the Code + +Let's take a moment to understand the last line because it could be a bit confusing and unfamiliar but it's what's make [`unjs/h3`](https://github.com/unjs/h3) so powerful. + +```js +import { createServer } from 'node:http' +import { createApp, toNodeListener } from 'h3' + +// ... + +createServer(toNodeListener(app)).listen(3000) +``` + +The fist thing we can notice is that the app is completely runtime agnostic so ready for the future. In fact, we create an app and pass it to a function that wraps it to make it compatible with Node.js. It's called an adapter and the concept is important. + +[`unjs/h3](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called `toWebHandler` that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. + +Secondly, we can notice that we use [`createServer`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) from `node:http`. The listener is not build-in [`unjs/h3](https://github.com/unjs/h3) because edge runtime don't need it and it's a very platform specific thing. + +In the same time, it's allow use to create our own listener to put him on steroids. In the ecosystem, it's called [`unjs/listhen`](https://github.com/unjs/listhen) 👂. + + + +All of this makes [`unjs/h3](https://github.com/unjs/h3) fully agnostic from the infrastructure and versatile to be used everywhere anytime. + +## Add a Router + +Now we have understand the basics, let's add a router to our app to make it more useful. In fact, the first example respond always with `Hello World!` but we want to respond with `Hello World!` only on the root path `/`. + +We can try with `/foo`: + +```bash +curl http://localhost:3000/foo +# Output: Hello World! +``` + +We expect to have a `404` error but we have a `200` response. It's because we don't have a router and the handler is called on every request. + +To fix it, create a new file named `routing.mjs`. Same as before, we will need to create an app and a server. + +```js [routing.mjs] +import { createServer } from 'node:http' +import { createApp, toNodeListener } from 'h3' + +const app = createApp() + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we will add a router to our app. It will be able to route requests to the right handler. + +```js [routing.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler(() => { + return 'Hello World!' + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +When a request is received, the router will try to match it with the right handler. In our case, we have only one handler for the root path `/`. + +Finally, we can run it: + +```bash +node routing.mjs +``` + +And test it: + +```bash +curl http://localhost:3000 +# Output: Hello World! +curl http://localhost:3000/foo +# Output: { "statusCode": 404, "statusMessage": "Cannot find any path matching /foo.", "stack": [] } +``` + +Perfect! It works as expected. + +### HTTP Methods + +Let's try `curl -X POST http://localhost:3000` and see what happens. + +We receive a `404` error. It's because we setup our handler only for `GET` requests using the `get` method of the router. + +To handle others HTTP methods, we can use the appropriate method: + +- `router.get` for `GET` requests +- `router.post` for `POST` requests +- `router.put` for `PUT` requests +- `router.patch` for `PATCH` requests +- `router.delete` for `DELETE` requests +- `router.head` for `HEAD` requests +- `router.options` for `OPTIONS` requests +- `router.trace` for `TRACE` requests +- `router.connect` for `CONNECT` requests +- `router.use` for all requests + +Perfect! To practice, we can add a new handler for `POST` requests: + +```js [routing.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler(() => { + return 'GET: Hello World!' + })) + .post('/', defineEventHandler(() => { + return 'POST: Hello World!' + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +We can chain handlers to avoid code duplication (`router.get(...).post(...)`). + +::alert{type="info"} +Do not forget to manually restart the server. +:: + +Then, we can test it: + +```bash +curl -X POST http://localhost:3000 +# Output: POST: Hello World! +``` + +Easy! :tada: + +## Composables + +### URL Params + +### Query Params + +### Body + +### Headers + +### Status + +### Redirects + +### Cookies + +### Errors + +## Hooks and Middlewares + + + + From 12ac7a28626e463e687669dd615263a829c3fa8a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 12 Sep 2023 23:20:03 +0000 Subject: [PATCH 2/8] [autofix.ci] apply automated fixes --- content/4.resources/1.learn/2.h3-101-first-hand.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md index 6e4521b0..9d97e046 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -103,7 +103,7 @@ createServer(toNodeListener(app)).listen(3000) The fist thing we can notice is that the app is completely runtime agnostic so ready for the future. In fact, we create an app and pass it to a function that wraps it to make it compatible with Node.js. It's called an adapter and the concept is important. -[`unjs/h3](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called `toWebHandler` that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. +[`unjs/h3](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called`toWebHandler`that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. Secondly, we can notice that we use [`createServer`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) from `node:http`. The listener is not build-in [`unjs/h3](https://github.com/unjs/h3) because edge runtime don't need it and it's a very platform specific thing. @@ -249,6 +249,4 @@ Easy! :tada: ## Hooks and Middlewares - - From 6f8e76570348d28b276b1d2dd43dcdf441bc8ef9 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Wed, 13 Sep 2023 01:22:16 +0200 Subject: [PATCH 3/8] chore: lint --- content/4.resources/1.learn/2.h3-101-first-hand.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md index 9d97e046..d5be824e 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -103,7 +103,7 @@ createServer(toNodeListener(app)).listen(3000) The fist thing we can notice is that the app is completely runtime agnostic so ready for the future. In fact, we create an app and pass it to a function that wraps it to make it compatible with Node.js. It's called an adapter and the concept is important. -[`unjs/h3](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called`toWebHandler`that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. +[`unjs/h3`](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called `toWebHandler` that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. Secondly, we can notice that we use [`createServer`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) from `node:http`. The listener is not build-in [`unjs/h3](https://github.com/unjs/h3) because edge runtime don't need it and it's a very platform specific thing. From 39eae8b5e439fc7acc09926ff9b516dfa31a22be Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Wed, 13 Sep 2023 16:30:30 +0200 Subject: [PATCH 4/8] content: add more about usage --- .../1.learn/2.h3-101-first-hand.md | 443 +++++++++++++++++- 1 file changed, 441 insertions(+), 2 deletions(-) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md index d5be824e..67fbbd92 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -231,22 +231,461 @@ Easy! :tada: ## Composables +To interact with the request, [`unjs/h3`](https://github.com/unjs/h3) give us an access to a parameter inside the callback function from `defineEventHandler`. + +To illustrate, let's take a little example: + +```js +import { createServer } from 'node:http' +import { createApp, defineEventHandler, toNodeListener } from 'h3' + +const app = createApp() + .use('/', defineEventHandler((event) => { + return 'Hello World!' + })) +``` + +Now, we have access to an object called `event` that will be used to get informations from the request and set informations for the response. + +The particularity of [`unjs/h3`](https://github.com/unjs/h3) is that we never have to directly interact with this `event` object. Everything is provided as composables and that's we will discover in this section. + ### URL Params +Let's start our composables tour with those related to the URL params. + +We can create a file named `url-params.mjs` to discover our first composable. Inside, we can create a simple app that will respond with the value of the `id` param. + +```js [url-params.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, getRouterParam, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/:name', defineEventHandler((event) => { + const name = getRouterParam(event, 'name') + + return `Hello ${name}!` + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +The parameter `name` is the name of the parameter in the URL and we could named it as we want. The only important thing is to use the same name in the handler. + +::alert{type="info"} +For the majority of composable, we will pass the `event` object as the first parameter. In fact, there is no magic with composables. We can achieve the same result with `event.context.params['name']`. But, don't do it! Sometimes, composables are much more than a simple getter. +:: + +Then, we can run it: + +```bash +node url-params.mjs +``` + +And test it: + +```bash +curl http://localhost:3000/unjs +# Output: Hello unjs +``` + +Of course, we can have set multiple URL parameters and get all at the same time with `getRouterParams`: + +```js [url-params.mjs] +// ... +import { getRouterParams } from 'h3' + +const router = createRouter() + .get('/:name/:age', defineEventHandler((event) => { + const params = getRouterParams(event) + // Similar to: + // const name = getRouterParam(event, 'name') + // const age = getRouterParam(event, 'age') + + return `Hello ${params.name}! You are ${params.age} years old.` + })) + +// ... +``` + +Simple, right? And the nice thing is that everything with [`unjs/h3`](https://github.com/unjs/h3) is like this! + ### Query Params +Getting is pretty straight forward since there is only one composable `getQuery`. + +```js [query-params.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, getQuery, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler((event) => { + const query = getQuery(event) + + return { + query + } + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node query-params.mjs +``` + +And test it: + +```bash +curl http://localhost:3000?name=unjs +# Output: { "query": { "name": "unjs" } } +``` + + + ### Body +Reading the body is also something very simple and we can read it as multiple formats. + +```js [body.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, readBody, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .post('/', defineEventHandler(async (event) => { + const body = await readBody(event) // Notice that we need to use await because readBody is async + + return { + body + } + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node body.mjs +``` + +And test it: + +```bash +curl -X POST http://localhost:3000 -d '{"name": "unjs"}' +# Output: { "body": { "name": "unjs" } } +``` + +This works well if the body is `application/json` or `application/x-www-form-urlencoded` or `multipart/form-data`? + +To solve this, we have access to more composables: + +- `readFormData`, return a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData), +- `readMultipleFormData`, return an array of `MultiPartData` to have access to uploaded files, +- `readRawBody`, return a `Buffer` or a `string`. + ### Headers +To interact with `headers`, we have many composables. + +Some of them are for the request: +- `getRequestHeader`, get the value of a header from the request using its name, +- `getRequestHeaders`, get all headers from the request as an object, + +And others for the response: +- `getResponseHeader`, get the value of a header from the response using its name, +- `getResponseHeaders`, get all headers from the response as an object, +- `setResponseHeader`, set the value of a header from the response using its name, +- `setResponseHeaders`, set all headers from the response using an object. +- `appendResponseHeader`, append the value of a header from the response using its name, +- `appendResponseHeaders`, append all headers from the response using an object. +- `removeResponseHeader`, remove the value of a header from the response using its name, +- `clearResponseHeaders`, clear all headers from the response or those provided as an array. + +Let's pratice with all of them! + +```js [headers.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, getRequestHeader, getResponseHeader, setResponseHeader, setResponseHeaders, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler((event) => { + const name = getRequestHeader(event, 'x-name') + + setResponseHeader(event, 'x-server', 'nitro') + setResponseHeaders(event, { 'x-name': name, 'x-unjs': 'amazing' }) + + const unjs = getResponseHeader(event, 'x-unjs') + + return { + name, + unjs + } + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node headers.mjs +``` + +And test it: + +```bash +curl -v http://localhost:3000 -H 'X-Name: unjs' +# Output: { "headers": { "x-name": "unjs" } } +``` + +In the same time, we display headers with `-v` and we can observe our `x-server` set to `h3`. + ### Status +Handling the status allow us to return correct informations to our client that can be used to provide useful informations to end-user. + +To do so, we have access to composables to get and set the status code and message. + +First, let's create a file `status.mjs`. Inside, we will set a status with it's message and get it later to return it. + +```js [status.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, getResponseStatus, getResponseStatusText, setResponseStatus, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/404', defineEventHandler((event) => { + + setResponseStatus(event, 404, 'The page you are looking for was not found.') // Message is optional. + + const status = getResponseStatus(event) + const statusMessage = getResponseStatusText(event) + + return { + status, + statusMessage + } + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node status.mjs +``` + +And test it: + +```bash +curl -v http://localhost:3000/404 +# Output: { "status": 404, "statusMessage": "The page you are looking for was not found." } +``` + +In the same time, we can observe, thanks to `-v` some headers like `HTTP/1.1 404 The page you are looking for was not found.` which show that everything works fine. + ### Redirects +Now we now how to control status, we can also handle some redirects! + +For this, we have access to a composable named `sendRedirect` which take the `event` object, a location and optionally, a status code (by default, it's a 301 Permanent Redirect). + +To try it, we can create a file named `redirect.mjs` and add a handler that redirect from `/` to `https://unjs.io` using a 302 Found: + +```js [redirect.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, sendRedirect, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler((event) => { + return sendRedirect(event, 'https://unjs.io', 302) // Do not forget to return, it's important. + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node redirect.mjs +``` + +And test it: + +```bash +curl -v http://localhost:3000 +# Output: +``` + +In the same time, we can observe, thanks to `-v` some headers like `location: https://unjs.io` and `HTTP/1.1 302 Found` which show that everything works fine. A redirect must be handled by the client. A browser will automatically follow the redirect and display the page. + ### Cookies +[`unjs/h3`](https://github.com/unjs/h3) handle natively cookies. It's a very important feature to build web applications to manage sessions, authentication, and more. + + + +To play with them, we have 3 importants composables: + +- `getCookie`, get the value of a cookie from the request using its name, +- `setCookie`, set the value of a cookie from the response using its name, +- `removeCookie`, remove the value of a cookie from the response using its name. + +That's it! Let's try it with a file named `cookies.mjs`: + +```js [cookies.mjs] +import { createServer } from 'node:http' +import { createApp, createRouter, defineEventHandler, getCookie, removeCookie, setCookie, toNodeListener } from 'h3' + +const app = createApp() + +const router = createRouter() + .get('/', defineEventHandler((event) => { + const name = getCookie(event, 'name') + + setCookie(event, 'server', 'h3', { httpOnly: true, secure: true }) + setCookie(event, 'age', '2', { httpOnly: true, secure: true }) + removeCookie(event, 'age') + + return { + name + } + })) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +The `setCookie` composable set `Path` to `/` by default but we can add any options supported by the [Set-Cookie header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie). + +Then, we can run it: + +```bash +node cookies.mjs +``` + +And test it: + +```bash +curl -v http://localhost:3000 -H 'Cookie: name=unjs' +# Output: { "cookies": { "name": "unjs" } } +``` + +In the same time, we can observe, thanks to `-v` some headers like `Set-Cookie: server=h3; Path=/; HttpOnly; Secure` which show that everything works fine. + ### Errors -## Hooks and Middlewares +Finally, we have access to composables to create errors. This can be useful when a user sent wrong data, a validation or a request failed. + +To create a new error, there is a composable called `createError`. The simplest way to call it by passing the error message `createError('Something wrong')`. + +But you can also pass an object with many options: + +- `status`, similar to `statusCode` used to set the HTTP status code for the response, +- `statusText`, similar to `statusMessage` or `message` used to set the HTTP status message for the response, +- `data`, used to provide a custom payload for the log, +- `stack`, used to provide a custom stack for the log, +- `cause`, used to provide a cause for the error, +- `fatal`, used to log the error and stop the execution of the handler. + +::alert{type="info"} +Note that `status` have an higher priority than `statusCode` and `statusText` have an higher priority than `statusMessage` or `message`. +:: + +We can create a little demo for these options: + +```js [errors.mjs] +import { createServer } from 'node:http' +import { + createApp, + createError, + createRouter, + defineEventHandler, + toNodeListener, +} from 'h3' + +const app = createApp() + +const router = createRouter() + .get( + '/', + defineEventHandler(() => { + // Do not forget to return the error + return createError('A simple error') // Create a 500 Internal Server Error by default + }), + ) + .get( + '/complexe-error', + defineEventHandler(() => { + // You can fully customize errors by adding data, cause and if it's a fatal error or not + return 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 + return createError({ + status: 500, + message: 'Fatal error', + fatal: true, + data: { foo: 'bar' }, + }) + }), + ) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000) +``` + +Then, we can run it: + +```bash +node errors.mjs +``` + +And test it: + +```bash +curl -v http://localhost:3000 +curl -v http://localhost:3000/complexe-error +curl -v http://localhost:3000/fatal-error +``` + +And _voilà_, we have a nice error handling! + +## A Lot More - + From a61b21ec0f8e7342b45fc62e0fe9e4b6462cd424 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 13 Sep 2023 14:31:36 +0000 Subject: [PATCH 5/8] [autofix.ci] apply automated fixes --- content/4.resources/1.learn/2.h3-101-first-hand.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md index 67fbbd92..91a30043 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -402,10 +402,12 @@ To solve this, we have access to more composables: To interact with `headers`, we have many composables. Some of them are for the request: + - `getRequestHeader`, get the value of a header from the request using its name, - `getRequestHeaders`, get all headers from the request as an object, And others for the response: + - `getResponseHeader`, get the value of a header from the response using its name, - `getResponseHeaders`, get all headers from the response as an object, - `setResponseHeader`, set the value of a header from the response using its name, From fbbbbaa07ebb65b48aec6a73b11fa9ae07b32af2 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Tue, 30 Jan 2024 22:35:29 +0100 Subject: [PATCH 6/8] content: udpate --- .../1.learn/2.h3-101-first-hand.md | 803 +++++------------- 1 file changed, 232 insertions(+), 571 deletions(-) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/4.resources/1.learn/2.h3-101-first-hand.md index 91a30043..56b5ff80 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/4.resources/1.learn/2.h3-101-first-hand.md @@ -1,693 +1,354 @@ --- -title: h3 101 - first hand -description: Discover h3, a better API for fetch that works on Node.js, browser, and workers. -authors: - - name: Estéban Soubiran - picture: https://esteban-soubiran.site/esteban.webp - twitter: soubiran_ -packages: - - h3 -publishedAt: 2023-09-12 -modifiedAt: 2023-09-12 -layout: learn-post +title: Build your first H3 app +description: Get started with H3 by building a simple app. +resources: + - # add a link to the examples repo + - # add a link to the documentation --- - -## Installation +H3 is a minimal http framework for high performance and portability. -First, let's create a new project: +During this tutorial, we will create a simple app to get a wide overview of H3 capabilities. This app will serve an HTML file populated with data. There will be some forms to add and remove data. At the end, we will see how to add an API endpoint to get the data in JSON format. -```bash -mkdir h3-101 -cd h3-101 -npm init -y -``` - -Then, install h3: - -```bash -npm install h3 -``` - -::alert{type="info"} -We can use the package manager of our choice like `npm`, `yarn`, `pnpm` or `bun`. -:: - -## First Server - -Before deep diving into [`unjs/h3`](https://github.com/unjs/h3), let's create the simpler server possible to understand how it works and some of its specificities. - -To do so, we can create our first file named `first-server.mjs`. +> [!NOTE] +> Deep dive into H3 through [the dedicated documentation](https://h3.unjs.io). -First of all, we will first create our app, where request are processed. +## Prerequisites -```js [first-server.mjs] -import { createApp } from 'h3' +To follow this tutorial, we need to have [Node.js](https://nodejs.org/en/) installed on our machine with [npm](https://www.npmjs.com/). We also need to have a basic knowledge of JavaScript. -const app = createApp() -``` - -Then, we will plug a event handler. It's a function that will be invoked on requests. In our case, we will response to every request with `Hello World!`. - -```js [first-server.mjs] -import { createApp, defineEventHandler } from 'h3' - -const app = createApp() - .use('/', defineEventHandler(() => { - return new Response('Hello World!') - })) -``` - -But our app does not listen requests, just handle them for the moment. To listen for requests to be handled, we will use [`node:http`](https://nodejs.org/api/http.html) to create a server and listen on port `3000`. +> [!NOTE] +> Despite H3 is written in TypeScript, you don't need to know TypeScript to use it. -```js [first-server.mjs] -import { createServer } from 'node:http' -import { createApp, defineEventHandler, toNodeListener } from 'h3' +## Create a New Project -const app = createApp() - .use('/', defineEventHandler(() => { - return new Response('Hello World!') - })) - -createServer(toNodeListener(app)).listen(3000) -``` - -Finally, we can run it with [`Node.js`](https://nodejs.org/en/): +First, let's create a new npm project: ```bash -node first-script.mjs -``` - -Then, we can use [`curl`](https://curl.se/) to test it: - -```bash -curl http://localhost:3000 -# Output: Hello World! -``` - -And it works! - -### Understanding the Code - -Let's take a moment to understand the last line because it could be a bit confusing and unfamiliar but it's what's make [`unjs/h3`](https://github.com/unjs/h3) so powerful. - -```js -import { createServer } from 'node:http' -import { createApp, toNodeListener } from 'h3' - -// ... - -createServer(toNodeListener(app)).listen(3000) -``` - -The fist thing we can notice is that the app is completely runtime agnostic so ready for the future. In fact, we create an app and pass it to a function that wraps it to make it compatible with Node.js. It's called an adapter and the concept is important. - -[`unjs/h3`](https://github.com/unjs/h3) provide another important wrapper for the [`Fetch API`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) called `toWebHandler` that get an [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) and return a [`Response](https://developer.mozilla.org/en-US/docs/Web/API/Response). It enable us to deploy [`unjs/h3](https://github.com/unjs/h3) on the edge to [`Cloudflare Workers`](https://workers.cloudflare.com/), [`Deno Deploy`](https://deno.com/deploy), [`Bun`](https://bun.sh/), [`Lagon`](https://lagon.app/), or more. - -Secondly, we can notice that we use [`createServer`](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) from `node:http`. The listener is not build-in [`unjs/h3](https://github.com/unjs/h3) because edge runtime don't need it and it's a very platform specific thing. - -In the same time, it's allow use to create our own listener to put him on steroids. In the ecosystem, it's called [`unjs/listhen`](https://github.com/unjs/listhen) 👂. - - - -All of this makes [`unjs/h3](https://github.com/unjs/h3) fully agnostic from the infrastructure and versatile to be used everywhere anytime. - -## Add a Router - -Now we have understand the basics, let's add a router to our app to make it more useful. In fact, the first example respond always with `Hello World!` but we want to respond with `Hello World!` only on the root path `/`. - -We can try with `/foo`: - -```bash -curl http://localhost:3000/foo -# Output: Hello World! -``` - -We expect to have a `404` error but we have a `200` response. It's because we don't have a router and the handler is called on every request. - -To fix it, create a new file named `routing.mjs`. Same as before, we will need to create an app and a server. - -```js [routing.mjs] -import { createServer } from 'node:http' -import { createApp, toNodeListener } from 'h3' - -const app = createApp() - -createServer(toNodeListener(app)).listen(3000) -``` - -Then, we will add a router to our app. It will be able to route requests to the right handler. - -```js [routing.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' - -const app = createApp() - -const router = createRouter() - .get('/', defineEventHandler(() => { - return 'Hello World!' - })) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) -``` - -When a request is received, the router will try to match it with the right handler. In our case, we have only one handler for the root path `/`. - -Finally, we can run it: - -```bash -node routing.mjs +mkdir my-h3-app +cd my-h3-app +npm init -y ``` -And test it: +Then, install H3: ```bash -curl http://localhost:3000 -# Output: Hello World! -curl http://localhost:3000/foo -# Output: { "statusCode": 404, "statusMessage": "Cannot find any path matching /foo.", "stack": [] } +npm install h3 ``` -Perfect! It works as expected. - -### HTTP Methods - -Let's try `curl -X POST http://localhost:3000` and see what happens. - -We receive a `404` error. It's because we setup our handler only for `GET` requests using the `get` method of the router. - -To handle others HTTP methods, we can use the appropriate method: +And that's it! We are ready to start coding. -- `router.get` for `GET` requests -- `router.post` for `POST` requests -- `router.put` for `PUT` requests -- `router.patch` for `PATCH` requests -- `router.delete` for `DELETE` requests -- `router.head` for `HEAD` requests -- `router.options` for `OPTIONS` requests -- `router.trace` for `TRACE` requests -- `router.connect` for `CONNECT` requests -- `router.use` for all requests +## Create the App -Perfect! To practice, we can add a new handler for `POST` requests: +To create our first H3 app, we need to create an `app.ts` file at the root of our project. Inside, we will create a new app by importing the `createApp` function from H3 and calling it: -```js [routing.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' +```ts [app.ts] +import { createApp } from 'h3'; -const app = createApp() - -const router = createRouter() - .get('/', defineEventHandler(() => { - return 'GET: Hello World!' - })) - .post('/', defineEventHandler(() => { - return 'POST: Hello World!' - })) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) +export const app = createApp(); ``` -We can chain handlers to avoid code duplication (`router.get(...).post(...)`). - -::alert{type="info"} -Do not forget to manually restart the server. -:: - -Then, we can test it: +:read-more{to="https://h3.unjs.io/concepts/app" title="App"} -```bash -curl -X POST http://localhost:3000 -# Output: POST: Hello World! -``` - -Easy! :tada: +Do not forget the `export` keyword, it's important for the listener. -## Composables +## Add a Listener -To interact with the request, [`unjs/h3`](https://github.com/unjs/h3) give us an access to a parameter inside the callback function from `defineEventHandler`. +Speaking of listener, our app is not able to respond to any request yet. To do so, we need to add a listener. A listener is used to listen an HTTP event, transfert it to our app and send back the response. -To illustrate, let's take a little example: +For our tutorial, we will use [unjs/listhen](https://listhen.unjs.io). -```js -import { createServer } from 'node:http' -import { createApp, defineEventHandler, toNodeListener } from 'h3' +In the `package.json` file, add a script named `start`: -const app = createApp() - .use('/', defineEventHandler((event) => { - return 'Hello World!' - })) +```json [package.json] +{ + "scripts": { + "start": "npx --yes listhen -w ./app.ts" + } +} ``` -Now, we have access to an object called `event` that will be used to get informations from the request and set informations for the response. +This script will start a server listening on port `3000` using our app and watching for changes. -The particularity of [`unjs/h3`](https://github.com/unjs/h3) is that we never have to directly interact with this `event` object. Everything is provided as composables and that's we will discover in this section. +We can now run the command `npm start` to start our server. -### URL Params +## Create a Router -Let's start our composables tour with those related to the URL params. +Now that our app is ready to accept HTTP requests, we need to create a router to handle them. The purpose of the router is to match the request to the right handler. -We can create a file named `url-params.mjs` to discover our first composable. Inside, we can create a simple app that will respond with the value of the `id` param. +With H3, we've just to use the function `createRouter` and add it to our app: -```js [url-params.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, getRouterParam, toNodeListener } from 'h3' - -const app = createApp() - -const router = createRouter() - .get('/:name', defineEventHandler((event) => { - const name = getRouterParam(event, 'name') +```ts [app.ts] +import { createApp, createRouter } from 'h3'; - return `Hello ${name}!` - })) +export const app = createApp(); -app.use(router) +const router = createRouter(); -createServer(toNodeListener(app)).listen(3000) +app.use(router); ``` -The parameter `name` is the name of the parameter in the URL and we could named it as we want. The only important thing is to use the same name in the handler. +The `app.use(router)`{lang="ts"} is necessary to add the router to our app. -::alert{type="info"} -For the majority of composable, we will pass the `event` object as the first parameter. In fact, there is no magic with composables. We can achieve the same result with `event.context.params['name']`. But, don't do it! Sometimes, composables are much more than a simple getter. -:: +:read-more{to="https://h3.unjs.io/concepts/router" title="Router"} -Then, we can run it: +## Add our First Handler -```bash -node url-params.mjs -``` - -And test it: +We have an app and a router. The only thing missing is the handlers. A handler is a function that will be called when a request matches the route. -```bash -curl http://localhost:3000/unjs -# Output: Hello unjs -``` +> [!NOTE] +> We may refer to controllers in other frameworks. -Of course, we can have set multiple URL parameters and get all at the same time with `getRouterParams`: +To add a handler, we can use any of the HTTP methods available on the router. For our tutorial, we will use the `get` method to handle the `GET` requests. -```js [url-params.mjs] +```ts [app.ts] // ... -import { getRouterParams } from 'h3' -const router = createRouter() - .get('/:name/:age', defineEventHandler((event) => { - const params = getRouterParams(event) - // Similar to: - // const name = getRouterParam(event, 'name') - // const age = getRouterParam(event, 'age') +const router = createRouter(); - return `Hello ${params.name}! You are ${params.age} years old.` - })) - -// ... +router.get('/', () => { + return 'Hello World!'; +}); ``` -Simple, right? And the nice thing is that everything with [`unjs/h3`](https://github.com/unjs/h3) is like this! +In the code above, we added a handler for the `/` route. This handler will send the string `Hello World!` to the client with a simple `return`{lang="ts"}. -### Query Params +:read-more{to="https://h3.unjs.io/concepts/event-handlers" title="Event Handlers"} -Getting is pretty straight forward since there is only one composable `getQuery`. +## Create a Fake Database -```js [query-params.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, getQuery, toNodeListener } from 'h3' +For our app, we will return an HTML page populated with some data. This part will not be explained in details since it's not the purpose of this tutorial. -const app = createApp() +To create our fake database (a JavaScript array) with some getters and setters, we need a file named `database.ts`: -const router = createRouter() - .get('/', defineEventHandler((event) => { - const query = getQuery(event) +```ts [database.ts] +import { Book } from "./types"; - return { - query - } - })) +/** + * This is a fake database since it's just an array of objects. + * + * For this example, it's sufficient but do not use this in production. + */ +const database: Book[] = [{ + title: "Anna Karenina", + price: 42, +}, { + title: "Madame Bovary", + price: 15, +}, { + title: "War and Peace", + price: 36, +}, { + title: "The Great Gatsby", + price: 87, +}, { + title: "Lolita", + price: 23, +} +]; -app.use(router) +export function getBooks(): Book[] { + return database; +} -createServer(toNodeListener(app)).listen(3000) -``` +export function addBook(book: Book) { + database.push(book); +} -Then, we can run it: +export function removeBook(title: string) { + const item = database.find((item) => item.title === title); -```bash -node query-params.mjs -``` + if (!item) { + return + } -And test it: + const index = database.indexOf(item); -```bash -curl http://localhost:3000?name=unjs -# Output: { "query": { "name": "unjs" } } + if (index > -1) { + database.splice(index, 1); + } +} ``` - - -### Body - -Reading the body is also something very simple and we can read it as multiple formats. - -```js [body.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, readBody, toNodeListener } from 'h3' - -const app = createApp() +Add some types on a file named `types.ts`: -const router = createRouter() - .post('/', defineEventHandler(async (event) => { - const body = await readBody(event) // Notice that we need to use await because readBody is async - - return { - body - } - })) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) +```ts [types.ts] +export interface Book { + title: string + price: number +} ``` -Then, we can run it: - -```bash -node body.mjs -``` - -And test it: - -```bash -curl -X POST http://localhost:3000 -d '{"name": "unjs"}' -# Output: { "body": { "name": "unjs" } } -``` - -This works well if the body is `application/json` or `application/x-www-form-urlencoded` or `multipart/form-data`? - -To solve this, we have access to more composables: - -- `readFormData`, return a [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData), -- `readMultipleFormData`, return an array of `MultiPartData` to have access to uploaded files, -- `readRawBody`, return a `Buffer` or a `string`. - -### Headers - -To interact with `headers`, we have many composables. - -Some of them are for the request: +> [!IMPORTANT] +> This is a fake database since it's just an array of objects. For this example, it's sufficient but **do not use this in production**. -- `getRequestHeader`, get the value of a header from the request using its name, -- `getRequestHeaders`, get all headers from the request as an object, +## Our First HTML Page -And others for the response: +For this first route, we will get the books from the database and render them in an HTML page. For each book, we will add a for to remove it from the database. Under the list, we will add a form to add a new book. -- `getResponseHeader`, get the value of a header from the response using its name, -- `getResponseHeaders`, get all headers from the response as an object, -- `setResponseHeader`, set the value of a header from the response using its name, -- `setResponseHeaders`, set all headers from the response using an object. -- `appendResponseHeader`, append the value of a header from the response using its name, -- `appendResponseHeaders`, append all headers from the response using an object. -- `removeResponseHeader`, remove the value of a header from the response using its name, -- `clearResponseHeaders`, clear all headers from the response or those provided as an array. +For the style, we will use [Pico CSS](https://picocss.com/). -Let's pratice with all of them! -```js [headers.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, getRequestHeader, getResponseHeader, setResponseHeader, setResponseHeaders, toNodeListener } from 'h3' - -const app = createApp() +```ts [app.ts] +// ... const router = createRouter() - .get('/', defineEventHandler((event) => { - const name = getRequestHeader(event, 'x-name') - - setResponseHeader(event, 'x-server', 'nitro') - setResponseHeaders(event, { 'x-name': name, 'x-unjs': 'amazing' }) - - const unjs = getResponseHeader(event, 'x-unjs') - - return { - name, - unjs - } - })) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) -``` - -Then, we can run it: - -```bash -node headers.mjs -``` -And test it: +router.get('/', defineEventHandler(() => { + const books = getBooks() + + return /* html */` + + + Books + + + +
+
+

Books

+
    + ${books.map((book) => /* html */` +
  • + ${book.title} - ${book.price} +
    + + +
    +
  • + `).join('')} +
+
+
+

Add a book

+
+ + + + + +
+
+
+ + + ` +})) +``` + +> [!NOTE] +> The comment `/* html */` is used by [a VS Code extension](https://marketplace.visualstudio.com/items?itemName=bierner.comment-tagged-templates) to provide syntax highlighting. + +Open a browser and go to `http://localhost:3000` to see the result. + +:read-more{to="https://h3.unjs.io/concepts/event-handlers#responses-types" title="Responses Types"} + +## Interact with the Database + +In our HTML page, we have two forms. One to add a book and one to remove a book. We need to add two new routes to handle them. + +### Add a Book + +To add a book, we need to handle a `POST` request on the `/add` route. We will also validate the data sent by the client using [Zod](https://zod.dev/) and we need to install it: ```bash -curl -v http://localhost:3000 -H 'X-Name: unjs' -# Output: { "headers": { "x-name": "unjs" } } +npm install zod ``` -In the same time, we display headers with `-v` and we can observe our `x-server` set to `h3`. +> [!NOTE] +> Zod is a TypeScript-first schema declaration and validation library. It's not mandatory to use it with H3 but it's a good practice to validate the data since it's runtime agnostic. -### Status - -Handling the status allow us to return correct informations to our client that can be used to provide useful informations to end-user. - -To do so, we have access to composables to get and set the status code and message. - -First, let's create a file `status.mjs`. Inside, we will set a status with it's message and get it later to return it. - -```js [status.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, getResponseStatus, getResponseStatusText, setResponseStatus, toNodeListener } from 'h3' - -const app = createApp() +Then, we can add the route: +```ts [app.ts] +import { z } from 'zod' +// ... const router = createRouter() - .get('/404', defineEventHandler((event) => { - - setResponseStatus(event, 404, 'The page you are looking for was not found.') // Message is optional. - - const status = getResponseStatus(event) - const statusMessage = getResponseStatusText(event) - - return { - status, - statusMessage - } - })) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) -``` - -Then, we can run it: -```bash -node status.mjs -``` +router.post('/add', defineEventHandler(async (event) => { + const body = await readValidatedBody(event, z.object({ + title: z.string(), + price: z.number({ coerce: true }).int().nonnegative(), + }).parse) -And test it: + addBook(body) -```bash -curl -v http://localhost:3000/404 -# Output: { "status": 404, "statusMessage": "The page you are looking for was not found." } + const referer = getHeader(event, 'referer') ?? '/' + return sendRedirect(event, referer) +})) ``` -In the same time, we can observe, thanks to `-v` some headers like `HTTP/1.1 404 The page you are looking for was not found.` which show that everything works fine. +There is two important things to notice in this code. -### Redirects +First, we use the `readValidatedBody` function to read the body of the request and validate it. It's important to validate the data sent by the client to avoid any security issue. -Now we now how to control status, we can also handle some redirects! +Second, we use the `sendRedirect` function to redirect the client to the previous page. We use the `referer` header to get the previous page. If the header is not present, we redirect to the root page. -For this, we have access to a composable named `sendRedirect` which take the `event` object, a location and optionally, a status code (by default, it's a 301 Permanent Redirect). +> [!NOTE] +> We should think to redirect to `/` but using the referer is a better practice if we change the root page. -To try it, we can create a file named `redirect.mjs` and add a handler that redirect from `/` to `https://unjs.io` using a 302 Found: +:read-more{to="https://h3.unjs.io/guides/validate-data" title="Validate Data"} -```js [redirect.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, sendRedirect, toNodeListener } from 'h3' +### Remove a Book -const app = createApp() +Nothing new here, we will handle a `POST` request on the `/remove` route: +```ts [app.ts] +// ... const router = createRouter() - .get('/', defineEventHandler((event) => { - return sendRedirect(event, 'https://unjs.io', 302) // Do not forget to return, it's important. - })) -app.use(router) +router.post('/remove', defineEventHandler(async (event) => { + const body = await readValidatedBody(event, z.object({ + title: z.string(), + }).parse) -createServer(toNodeListener(app)).listen(3000) -``` - -Then, we can run it: + removeBook(body.title) -```bash -node redirect.mjs + const referer = getHeader(event, 'referer') ?? '/' + return sendRedirect(event, referer) +})) ``` -And test it: - -```bash -curl -v http://localhost:3000 -# Output: -``` - -In the same time, we can observe, thanks to `-v` some headers like `location: https://unjs.io` and `HTTP/1.1 302 Found` which show that everything works fine. A redirect must be handled by the client. A browser will automatically follow the redirect and display the page. - -### Cookies - -[`unjs/h3`](https://github.com/unjs/h3) handle natively cookies. It's a very important feature to build web applications to manage sessions, authentication, and more. - - - -To play with them, we have 3 importants composables: - -- `getCookie`, get the value of a cookie from the request using its name, -- `setCookie`, set the value of a cookie from the response using its name, -- `removeCookie`, remove the value of a cookie from the response using its name. - -That's it! Let's try it with a file named `cookies.mjs`: - -```js [cookies.mjs] -import { createServer } from 'node:http' -import { createApp, createRouter, defineEventHandler, getCookie, removeCookie, setCookie, toNodeListener } from 'h3' - -const app = createApp() - -const router = createRouter() - .get('/', defineEventHandler((event) => { - const name = getCookie(event, 'name') +Same as before, we use the `readValidatedBody` function to read the body of the request and validate it. Then, we use the `sendRedirect` function to redirect the client to the previous page. - setCookie(event, 'server', 'h3', { httpOnly: true, secure: true }) - setCookie(event, 'age', '2', { httpOnly: true, secure: true }) - removeCookie(event, 'age') +:read-more{to="https://h3.unjs.io/guides/validate-data" title="Validate Data"} - return { - name - } - })) +## Add an API Endpoint -app.use(router) +We would need to add an API endpoint for external services. For this example, we will create another router dedicated to the API. -createServer(toNodeListener(app)).listen(3000) +```ts [app.ts] +// ... +const apiRouter = createRouter() ``` -The `setCookie` composable set `Path` to `/` by default but we can add any options supported by the [Set-Cookie header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie). - -Then, we can run it: - -```bash -node cookies.mjs -``` +Like any router, we will add an handler for the `/books` route: -And test it: +```ts [app.ts] +// ... +apiRouter.get('/books', defineEventHandler(() => { + const books = getBooks() -```bash -curl -v http://localhost:3000 -H 'Cookie: name=unjs' -# Output: { "cookies": { "name": "unjs" } } + return books +})) ``` -In the same time, we can observe, thanks to `-v` some headers like `Set-Cookie: server=h3; Path=/; HttpOnly; Secure` which show that everything works fine. - -### Errors - -Finally, we have access to composables to create errors. This can be useful when a user sent wrong data, a validation or a request failed. - -To create a new error, there is a composable called `createError`. The simplest way to call it by passing the error message `createError('Something wrong')`. - -But you can also pass an object with many options: - -- `status`, similar to `statusCode` used to set the HTTP status code for the response, -- `statusText`, similar to `statusMessage` or `message` used to set the HTTP status message for the response, -- `data`, used to provide a custom payload for the log, -- `stack`, used to provide a custom stack for the log, -- `cause`, used to provide a cause for the error, -- `fatal`, used to log the error and stop the execution of the handler. +Then, we will bind this second router to the first one using a base path: -::alert{type="info"} -Note that `status` have an higher priority than `statusCode` and `statusText` have an higher priority than `statusMessage` or `message`. -:: - -We can create a little demo for these options: - -```js [errors.mjs] -import { createServer } from 'node:http' -import { - createApp, - createError, - createRouter, - defineEventHandler, - toNodeListener, -} from 'h3' - -const app = createApp() - -const router = createRouter() - .get( - '/', - defineEventHandler(() => { - // Do not forget to return the error - return createError('A simple error') // Create a 500 Internal Server Error by default - }), - ) - .get( - '/complexe-error', - defineEventHandler(() => { - // You can fully customize errors by adding data, cause and if it's a fatal error or not - return 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 - return createError({ - status: 500, - message: 'Fatal error', - fatal: true, - data: { foo: 'bar' }, - }) - }), - ) - -app.use(router) - -createServer(toNodeListener(app)).listen(3000) +```ts [app.ts] +// ... +router.use('/api/**', useBase('/api', apiRouter.handler)) ``` -Then, we can run it: +This means that every `/api/` route will be handled by the `apiRouter` and this second router will be called with the path without the `/api` prefix to find the right handler. -```bash -node errors.mjs -``` - -And test it: +:read-more{to="https://h3.unjs.io/concepts/router#nested-routers" title="Nested Routers"} -```bash -curl -v http://localhost:3000 -curl -v http://localhost:3000/complexe-error -curl -v http://localhost:3000/fatal-error -``` +## Conclusion -And _voilà_, we have a nice error handling! +And voilà! We now have our first H3 app! -## A Lot More +During this course, we saw how to create a H3 app, use a listener with it, create a router, add handlers, validate data. But there is a lot more to discover about H3 on [the dedicated documentation](https://h3.unjs.io). - +Then, do not hesitate to take a look at [Nitro](https://nitro.unjs.io) to create more advanced web servers that run everywhere. From 57562be8a3d26a709ec8e9f45b08c2383a849e97 Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Feb 2024 18:11:30 +0100 Subject: [PATCH 7/8] content: update h3 getting started --- .../2024-02-11-build-your-first-h3-app.md} | 201 ++++++++---------- 1 file changed, 86 insertions(+), 115 deletions(-) rename content/{4.resources/1.learn/2.h3-101-first-hand.md => learn/articles/2024-02-11-build-your-first-h3-app.md} (65%) diff --git a/content/4.resources/1.learn/2.h3-101-first-hand.md b/content/learn/articles/2024-02-11-build-your-first-h3-app.md similarity index 65% rename from content/4.resources/1.learn/2.h3-101-first-hand.md rename to content/learn/articles/2024-02-11-build-your-first-h3-app.md index 56b5ff80..067093e0 100644 --- a/content/4.resources/1.learn/2.h3-101-first-hand.md +++ b/content/learn/articles/2024-02-11-build-your-first-h3-app.md @@ -1,25 +1,43 @@ --- title: Build your first H3 app description: Get started with H3 by building a simple app. +authors: + - name: Estéban S + picture: https://github.com/barbapapazes.png + twitter: soubiran_ +category: getting-started +packages: + - h3 resources: - - # add a link to the examples repo - - # add a link to the documentation + - label: Source Code + to: https://github.com/unjs/examples/tree/main/h3/build-your-first-app + icon: i-simple-icons-github + - label: H3 Documentation + to: https://h3.unjs.io + icon: i-heroicons-book-open + - label: H3 Examples + to: https://github.com/unjs/h3/tree/main/examples + icon: i-simple-icons-github +publishedAt: 2024-02-11 +modifiedAt: 2024-02-11 --- - H3 is a minimal http framework for high performance and portability. -During this tutorial, we will create a simple app to get a wide overview of H3 capabilities. This app will serve an HTML file populated with data. There will be some forms to add and remove data. At the end, we will see how to add an API endpoint to get the data in JSON format. +During this tutorial, we will create a very simple app to get a wide overview of H3 capabilities. This app will serve an HTML file populated with data. There will be some forms to add and remove data. At the end, we will see how to add an API endpoint to get the data in JSON format. > [!NOTE] > Deep dive into H3 through [the dedicated documentation](https://h3.unjs.io). +> [!TIP] +> For more complexe apps, take a look at [Nitro](https://nitro.unjs.io). + ## Prerequisites To follow this tutorial, we need to have [Node.js](https://nodejs.org/en/) installed on our machine with [npm](https://www.npmjs.com/). We also need to have a basic knowledge of JavaScript. > [!NOTE] -> Despite H3 is written in TypeScript, you don't need to know TypeScript to use it. +> Despite H3 is written in TypeScript, we don't need to know TypeScript to use it. ## Create a New Project @@ -41,12 +59,12 @@ And that's it! We are ready to start coding. ## Create the App -To create our first H3 app, we need to create an `app.ts` file at the root of our project. Inside, we will create a new app by importing the `createApp` function from H3 and calling it: +To create our first H3 app, we need to create an `app.mjs` file at the root of our project. Inside, we will create a new app by importing the `createApp` function from H3 and calling it: -```ts [app.ts] -import { createApp } from 'h3'; +```js [app.mjs] +import { createApp } from 'h3' -export const app = createApp(); +export const app = createApp() ``` :read-more{to="https://h3.unjs.io/concepts/app" title="App"} @@ -64,7 +82,7 @@ In the `package.json` file, add a script named `start`: ```json [package.json] { "scripts": { - "start": "npx --yes listhen -w ./app.ts" + "start": "npx --yes listhen -w ./app.mjs" } } ``` @@ -79,17 +97,17 @@ Now that our app is ready to accept HTTP requests, we need to create a router to With H3, we've just to use the function `createRouter` and add it to our app: -```ts [app.ts] -import { createApp, createRouter } from 'h3'; +```js [app.mjs] +import { createApp, createRouter } from 'h3' -export const app = createApp(); +export const app = createApp() -const router = createRouter(); +const router = createRouter() -app.use(router); +app.use(router) ``` -The `app.use(router)`{lang="ts"} is necessary to add the router to our app. +The `app.use(router)`{lang="js"} is necessary to add the router to our app. :read-more{to="https://h3.unjs.io/concepts/router" title="Router"} @@ -102,114 +120,51 @@ We have an app and a router. The only thing missing is the handlers. A handler i To add a handler, we can use any of the HTTP methods available on the router. For our tutorial, we will use the `get` method to handle the `GET` requests. -```ts [app.ts] +```js [app.mjs] // ... -const router = createRouter(); +const router = createRouter() router.get('/', () => { - return 'Hello World!'; -}); -``` - -In the code above, we added a handler for the `/` route. This handler will send the string `Hello World!` to the client with a simple `return`{lang="ts"}. - -:read-more{to="https://h3.unjs.io/concepts/event-handlers" title="Event Handlers"} - -## Create a Fake Database - -For our app, we will return an HTML page populated with some data. This part will not be explained in details since it's not the purpose of this tutorial. - -To create our fake database (a JavaScript array) with some getters and setters, we need a file named `database.ts`: - -```ts [database.ts] -import { Book } from "./types"; - -/** - * This is a fake database since it's just an array of objects. - * - * For this example, it's sufficient but do not use this in production. - */ -const database: Book[] = [{ - title: "Anna Karenina", - price: 42, -}, { - title: "Madame Bovary", - price: 15, -}, { - title: "War and Peace", - price: 36, -}, { - title: "The Great Gatsby", - price: 87, -}, { - title: "Lolita", - price: 23, -} -]; + return 'Hello World!' +}) -export function getBooks(): Book[] { - return database; -} - -export function addBook(book: Book) { - database.push(book); -} - -export function removeBook(title: string) { - const item = database.find((item) => item.title === title); - - if (!item) { - return - } - - const index = database.indexOf(item); - - if (index > -1) { - database.splice(index, 1); - } -} +// ... ``` -Add some types on a file named `types.ts`: - -```ts [types.ts] -export interface Book { - title: string - price: number -} -``` +In the code above, we added a handler for the `/` route. This handler will send the string `Hello World!` to the client with a simple `return`{lang="js"}. -> [!IMPORTANT] -> This is a fake database since it's just an array of objects. For this example, it's sufficient but **do not use this in production**. +:read-more{to="https://h3.unjs.io/concepts/event-handlers" title="Event Handlers"} ## Our First HTML Page -For this first route, we will get the books from the database and render them in an HTML page. For each book, we will add a for to remove it from the database. Under the list, we will add a form to add a new book. +For this first route, we will get the books from a static array and render them in an HTML page. For each book, we will add a for to remove it from the database. Under the list, we will add a form to add a new book. For the style, we will use [Pico CSS](https://picocss.com/). - -```ts [app.ts] +```js [app.mjs] // ... const router = createRouter() -router.get('/', defineEventHandler(() => { - const books = getBooks() +const books = [ + { title: 'The Hobbit', price: 10 }, + { title: 'The Lord of the Rings', price: 20 }, +] +router.get('/', defineEventHandler(() => { return /* html */` Books - +

Books

    - ${books.map((book) => /* html */` + ${books.map(book => /* html */`
  • ${book.title} - ${book.price}
    @@ -244,9 +199,9 @@ Open a browser and go to `http://localhost:3000` to see the result. :read-more{to="https://h3.unjs.io/concepts/event-handlers#responses-types" title="Responses Types"} -## Interact with the Database +## Use POST Requests -In our HTML page, we have two forms. One to add a book and one to remove a book. We need to add two new routes to handle them. +In our HTML page, we have two forms. One to add a book and one to remove a book. We need to add two new routes to handle them. This is interesting because we will need to handle the body of the request. ### Add a Book @@ -257,22 +212,26 @@ npm install zod ``` > [!NOTE] -> Zod is a TypeScript-first schema declaration and validation library. It's not mandatory to use it with H3 but it's a good practice to validate the data since it's runtime agnostic. +> Zod is a schema validation with TypeScript type inference. It's not mandatory to use it with H3 but it's a recommended practice to validate the user data. Then, we can add the route: -```ts [app.ts] -import { z } from 'zod' +```js [app.mjs] +import zod from 'zod' // ... const router = createRouter() +const books = [ + // ... +] + router.post('/add', defineEventHandler(async (event) => { - const body = await readValidatedBody(event, z.object({ - title: z.string(), - price: z.number({ coerce: true }).int().nonnegative(), + const body = await readValidatedBody(event, zod.object({ + title: zod.string(), + price: zod.number({ coerce: true }).int().nonnegative(), }).parse) - addBook(body) + books.push(body) const referer = getHeader(event, 'referer') ?? '/' return sendRedirect(event, referer) @@ -283,6 +242,9 @@ There is two important things to notice in this code. First, we use the `readValidatedBody` function to read the body of the request and validate it. It's important to validate the data sent by the client to avoid any security issue. +> [!NOTE] +> We can use the `readBody` function to read the body of the request without validation. + Second, we use the `sendRedirect` function to redirect the client to the previous page. We use the `referer` header to get the previous page. If the header is not present, we redirect to the root page. > [!NOTE] @@ -290,20 +252,28 @@ Second, we use the `sendRedirect` function to redirect the client to the previou :read-more{to="https://h3.unjs.io/guides/validate-data" title="Validate Data"} +> [!IMPORTANT] +> For more advanced apps, we should use a database to store the data. Take a look at [Nitro](https://nitro.unjs.io) to achieve this with ease. + ### Remove a Book -Nothing new here, we will handle a `POST` request on the `/remove` route: +Nothing new here, we will handle a `POST` request on the `/remove` route, validate the data and remove the book from the array: -```ts [app.ts] +```js [app.mjs] +import zod from 'zod' // ... const router = createRouter() +const books = [ + // ... +] + router.post('/remove', defineEventHandler(async (event) => { - const body = await readValidatedBody(event, z.object({ - title: z.string(), + const body = await readValidatedBody(event, zod.object({ + title: zod.string(), }).parse) - removeBook(body.title) + books.splice(books.findIndex(book => book.title === body.title), 1) const referer = getHeader(event, 'referer') ?? '/' return sendRedirect(event, referer) @@ -314,29 +284,30 @@ Same as before, we use the `readValidatedBody` function to read the body of the :read-more{to="https://h3.unjs.io/guides/validate-data" title="Validate Data"} +> [!IMPORTANT] +> For more advanced apps, we should use a database to store the data. Take a look at [Nitro](https://nitro.unjs.io) to achieve this with ease. + ## Add an API Endpoint We would need to add an API endpoint for external services. For this example, we will create another router dedicated to the API. -```ts [app.ts] +```js [app.mjs] // ... const apiRouter = createRouter() ``` Like any router, we will add an handler for the `/books` route: -```ts [app.ts] +```js [app.mjs] // ... apiRouter.get('/books', defineEventHandler(() => { - const books = getBooks() - return books })) ``` Then, we will bind this second router to the first one using a base path: -```ts [app.ts] +```js [app.mjs] // ... router.use('/api/**', useBase('/api', apiRouter.handler)) ``` @@ -351,4 +322,4 @@ And voilĂ ! We now have our first H3 app! During this course, we saw how to create a H3 app, use a listener with it, create a router, add handlers, validate data. But there is a lot more to discover about H3 on [the dedicated documentation](https://h3.unjs.io). -Then, do not hesitate to take a look at [Nitro](https://nitro.unjs.io) to create more advanced web servers that run everywhere. +However, H3 is very low level and has very specific use cases. In general, we recommend [Nitro](https://nitro.unjs.io) to create more advanced web servers with database that run everywhere. From 4ddf8b8177b7a205a60de2a8d6ae26512a3b626c Mon Sep 17 00:00:00 2001 From: barbapapazes Date: Sat, 17 Feb 2024 18:15:57 +0100 Subject: [PATCH 8/8] chore: lint --- content/learn/articles/2024-02-11-build-your-first-h3-app.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/content/learn/articles/2024-02-11-build-your-first-h3-app.md b/content/learn/articles/2024-02-11-build-your-first-h3-app.md index 067093e0..e99de3df 100644 --- a/content/learn/articles/2024-02-11-build-your-first-h3-app.md +++ b/content/learn/articles/2024-02-11-build-your-first-h3-app.md @@ -24,11 +24,11 @@ modifiedAt: 2024-02-11 H3 is a minimal http framework for high performance and portability. -During this tutorial, we will create a very simple app to get a wide overview of H3 capabilities. This app will serve an HTML file populated with data. There will be some forms to add and remove data. At the end, we will see how to add an API endpoint to get the data in JSON format. - > [!NOTE] > Deep dive into H3 through [the dedicated documentation](https://h3.unjs.io). +During this tutorial, we will create a very simple app to get a wide overview of H3 capabilities. This app will serve an HTML file populated with data. There will be some forms to add and remove data. At the end, we will see how to add an API endpoint to get the data in JSON format. + > [!TIP] > For more complexe apps, take a look at [Nitro](https://nitro.unjs.io).