diff --git a/app.config.ts b/app.config.ts index ce301030..1ead43fd 100644 --- a/app.config.ts +++ b/app.config.ts @@ -126,24 +126,30 @@ export default defineAppConfig({ { title: 'Content', items: [ - // { - // title: 'Learn', - // url: '/learn', - // rel: null, - // target: null, - // }, - // { - // title: 'Build', - // url: '/build', - // rel: null, - // target: null, - // }, - // { - // title: 'Explore', - // url: '/explore', - // rel: null, - // target: null, - // }, + { + title: 'Resouces', + url: '/resources', + rel: null, + target: null, + }, + { + title: 'Learn', + url: '/resources/learn', + rel: null, + target: null, + }, + // { + // title: 'Build', + // url: '/build', + // rel: null, + // target: null, + // }, + // { + // title: 'Explore', + // url: '/explore', + // rel: null, + // target: null, + // }, { title: 'Search', url: '/search', @@ -161,6 +167,12 @@ export default defineAppConfig({ rel: 'noopener', target: '_blank', }, + { + title: 'X', + url: 'https://x.com/unjsio', + rel: 'noopener', + target: '_blank', + }, { title: 'GitHub', url: 'https://github.com/unjs', diff --git a/content/3.resources.yml b/content/3.resources.yml new file mode 100644 index 00000000..8ee81ad5 --- /dev/null +++ b/content/3.resources.yml @@ -0,0 +1,4 @@ +title: Resources +description: Get started with UnJS, craft a project from scratch and deep dive into into packages to learn how they are built. +navigation.icon: i-heroicons-beaker-solid +layout: resources diff --git a/content/3.resources/1.learn.yml b/content/3.resources/1.learn.yml new file mode 100644 index 00000000..420b01a1 --- /dev/null +++ b/content/3.resources/1.learn.yml @@ -0,0 +1,5 @@ +title: Learn the basics +description: Embark on a journey through this vast ecosystem, unraveling the mysteries and unlocking the true potential of more than twenty amazing packages. +navigation.icon: i-heroicons-rocket-launch-solid +navigation.description: Embark on a journey through this vast ecosystem +layout: learn diff --git a/content/3.resources/1.learn/1.ofetch-101-first-hand.md b/content/3.resources/1.learn/1.ofetch-101-first-hand.md new file mode 100644 index 00000000..e840fc64 --- /dev/null +++ b/content/3.resources/1.learn/1.ofetch-101-first-hand.md @@ -0,0 +1,421 @@ +--- +title: ofetch 101 - first hand +description: Discover ofetch, a better API for fetch that works on Node.js, browser, and workers. +authors: + - name: Barbapapazes + picture: https://esteban-soubiran.site/esteban.webp + twitter: soubiran_ +packages: + - ofetch +project: https://github.com/unjs/ofetch/tree/main/examples +publishedAt: 2023-11-04 +modifiedAt: 2023-11-04 +layout: article +--- + +[ofetch](https://github.com/unjs/ofetch) is a utility package to make HTTP requests. It exists to unify the API between Node.js, browser and workers and is built on top of the [fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API). + +In fact, Node.js [fully supports the fetch API since version 18](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#browser_compatibility) via the [unidici project](https://github.com/nodejs/undici). If we want to support older versions, we will need to use a third-party package. + +At the same time, we want to be able to fetch in every environment with the same code. Code could start running in a worker and then in a browser. + +[ofetch](https://github.com/unjs/ofetch) has been created to simplify these tasks and provide a wrapper around the native fetch API with more options and features. + +## Installation + +First, let's create a new project: + +```bash +mkdir ofetch-101 +cd ofetch-101 +npm init -y +``` + +Then, install the package: + +```bash +npm install ofetch +``` + +::alert{type="info"} +We can use the package manager of our choice like `npm`, `yarn`, `pnpm` or `bun`. +:: + +## First request + +In order to make our first request, let's create a file named `first-request.mjs` with the following content: + +```js [first-request.mjs] +import { ofetch } from 'ofetch' + +const data = await ofetch('https://ungh.cc/repos/unjs/ofetch') +``` + +Then, we can run the script with: + +```bash +node first-request.mjs +``` + +And _voilà_, we should see something like this: + +```sh +{ + repo: { + id: 319960543, + name: 'ofetch', + repo: 'unjs/ofetch', + description: '😱 A better fetch API. Works on node, browser and workers.', + createdAt: '2020-12-09T13:11:38Z', + updatedAt: '2023-09-04T17:23:36Z', + pushedAt: '2023-09-04T21:05:19Z', + stars: 2474, + watchers: 14, + forks: 80, + defaultBranch: 'main' + } +} +``` + +We have made our first request with [ofetch](https://github.com/unjs/ofetch)! :tada: Easy, right? + +### Manual parsing + +In our first example, the returned result is automatically parsed to JSON thanks to [unjs/destr](https://github.com/unjs/destr). + +<!-- TODO: add related article to 101 destr --> + +This behavior is very useful but sometimes, we want to manually parse the result. + +The first way to change how received data is parsed is using the `responseType` option. [ofetch](https://github.com/unjs/ofetch) provides several options: + +- `text` to have the raw text +- `json` to have a JSON object +- `blob` to have a `Blob` object +- `arrayBuffer` to have an `ArrayBuffer` object +- `stream` to have a `ReadableStream` object + +To try it, let's create a new file named `manual-parsing.mjs`. + +```js [manual-parsing.mjs] +import { ofetch } from 'ofetch' + +const data = await ofetch('https://ungh.cc/repos/unjs/ofetch', { + responseType: 'blob', +}) + +console.log(data) // A Blob object is returned. +``` + +We can also provide a custom function to parse the result using the `parseFunction` option. By default, [ofetch](https://github.com/unjs/ofetch) uses [unjs/destr](https://github.com/unjs/destr). Let's make our custom parser by creating a new file named `custom-parsing.mjs`. + +```js [custom-parsing.mjs] +import { ofetch } from 'ofetch' + +const data = await ofetch('https://ungh.cc/repos/unjs/ofetch', { + parseFunction: (data) => { + return data // We return the raw data but we could use JSON.parse to return a JSON object. + }, +}) + +console.log(data) // The data is not parsed. +``` + +The function takes the raw data as an argument and must return something, ideally, data parsed. + +## Type safety + +::alert{type="info"} +Usage of TypeScript is not mandatory to use [ofetch](https://github.com/unjs/ofetch). +:: + +Thanks to TypeScript and generics, [ofetch](https://github.com/unjs/ofetch) can be type safe. In order to easily use TypeScript to run our scripts, we will use [unjs/jiti](https://github.com/unjs/jiti). + +<!-- TODO: add related article to 101 jiti --> + +We can use it using `npx`: + +```bash +npx jiti <filename>.ts +``` + +Now that we are ready, let's create another file called `type-safety.ts` to type safe our [ofetch](https://github.com/unjs/ofetch) request: + +```bash +touch type-safety.ts +``` + +Then, let's create an interface for the response. We will use the same request as in our first request. + +```ts [type-safety.ts] +interface Repo { + id: number + name: string + repo: string + description: string + stars: number +} +``` + +Now, we can use the [ofetch](https://github.com/unjs/ofetch) function, like in our `first-request.ts` file, but this time, we will add a generic type to it. + +```ts [type-safety.ts] +import { ofetch } from 'ofetch' + +interface Repo { + id: number + name: string + repo: string + description: string + stars: number +} + +async function main() { + const { repo } = await ofetch<{ repo: Repo }>('https://ungh.cc/api/github/unjs/ofetch') + + console.log(`The repo ${repo.name} has ${repo.stars} stars.`) // The repo object is now strongly typed. +} + +main().catch(console.error) +``` + +Nothing more to have a fully typed repo object! :ok_hand: + +::alert{type="warning"} +This is **not a validation**. It's just a way to tell TypeScript what the response should look like. If we want full validation, we can use a tool like [zod](https://zod.dev) or [valibot](https://valibot.dev). +:: + +## Options + +With [ofetch](https://github.com/unjs/ofetch), we can do much more than simple `GET` requests. Let's see how to use and combine options to make more complex requests. + +### Methods + +[ofetch](https://github.com/unjs/ofetch) supports all the HTTP methods. Let's see how to use one of them. To test a `POST` request, we will use the GitHub API. Let's create a new file named `methods.mjs` with the following content: + +```js [methods.mjs] +import { ofetch } from 'ofetch' + +const response = await ofetch('https://api.github.com/gists', { + method: 'POST', +}) // Be careful, we use the GitHub API. + +console.log(response) +``` + +Of course, we receive an `401 Unauthorized` error because we didn't provide any authentication. But this error means we have successfully made a `POST` request. + +Supported methods are: + +- `GET` +- `POST` +- `PUT` +- `PATCH` +- `DELETE` +- `HEAD` +- `OPTIONS` + +### Body + +Of course, when the method is not `GET` or `HEAD`, we can provide a body to the request. Let's see how to do it by creating a new file named `body.mjs` with the following content: + +```js [body.mjs] +import { ofetch } from 'ofetch' + +const response = await ofetch('https://api.github.com/markdown', { + method: 'POST', + // To provide a body, we need to use the `body` option and just use an object. + body: { + text: 'UnJS is **awesome**!\n\nCheck out their [website](https://unjs.io).', + }, +}) // Be careful, we use the GitHub API. + +console.log(response) +``` + +And we receive: + +```html +<p>UnJS is <strong>awesome</strong>!</p> +<p>Check out their <a href="https://unjs.io" rel="nofollow">website</a>.</p> +``` + +Perfect and easy! + +The body can take several formats: + +- `string` +- `FormData` +- `URLSearchParams` +- `Blob` +- `BufferSource` +- `ReadableStream` +- `Record<string, any>` + +### Query string + +We can also provide some query string parameters to the request. They can be useful to add filters or to paginate the results. + +Imagine we want to get the last 2 tags of the [`unjs/ofetch`](https://github.com/unjs/ofetch) repository. We can do it by creating a new file named `query-string.mjs` with the following content: + +```js [query-string.mjs] +import { ofetch } from 'ofetch' + +const response = await ofetch('https://api.github.com/repos/unjs/ofetch/tags', { + query: { + per_page: 2, + }, +}) // Be careful, we use the GitHub API directly. + +console.log(response) +``` + +We receive this response (results could be different, because the tags are updated): + +```js +[ + { + name: 'v1.3.3', + zipball_url: 'https://api.github.com/repos/unjs/ofetch/zipball/refs/tags/v1.3.3', + tarball_url: 'https://api.github.com/repos/unjs/ofetch/tarball/refs/tags/v1.3.3', + commit: { + sha: '051ef83ceba6cc496ac487969c9f6611ef6bd10d', + url: 'https://api.github.com/repos/unjs/ofetch/commits/051ef83ceba6cc496ac487969c9f6611ef6bd10d' + }, + node_id: 'MDM6UmVmMzE5OTYwNTQzOnJlZnMvdGFncy92MS4zLjM=' + }, + { + name: 'v1.3.2', + zipball_url: 'https://api.github.com/repos/unjs/ofetch/zipball/refs/tags/v1.3.2', + tarball_url: 'https://api.github.com/repos/unjs/ofetch/tarball/refs/tags/v1.3.2', + commit: { + sha: '1258d51494656c9507a8bb82646c644c64f23e69', + url: 'https://api.github.com/repos/unjs/ofetch/commits/1258d51494656c9507a8bb82646c644c64f23e69' + }, + node_id: 'MDM6UmVmMzE5OTYwNTQzOnJlZnMvdGFncy92MS4zLjI=' + } +] +``` + +A query string is an object of type `{ [key: string]: any }` so it's easy to use. + +::alert{type="info"} +We can find a key `params` in the options. It's an alias for `query`. +:: + +### Headers + +In the [methods section](#methods), we encountered an error because we didn't provide any authentication. With headers, we can solve our problem. In fact, we need to provide to GitHub an authentication token in a header named `Authorization` with a value of `token <token>`. Let's see how to do it by creating a new file named `headers.mjs` with the following content: + +```js [headers.mjs] +import { ofetch } from 'ofetch' + +const response = await ofetch('https://api.github.com/gists', { + method: 'POST', + headers: { + Authorization: `token ${process.env.GH_TOKEN}`, + } +}) // Be careful, we use the GitHub API directly. + +console.log(response) +``` + +::alert{type="info"} +We can generate a new GitHub Token with the [GitHub CLI](https://cli.github.com): `gh auth token`. +:: + +We can run the script with: + +```bash +GH_TOKEN=<token> node headers.mjs +``` + +And we get the following error: + +```bash +FetchError: [POST] "https://api.github.com/gists": 422 Unprocessable Entity +``` + +And that's a good thing! It means we have successfully authenticated to GitHub. We just need to provide a valid body to the request but now, we know how to do it! We have to add the `body` option to the request with correct data: + +```js [headers.mjs] +import { ofetch } from 'ofetch' + +const response = await ofetch('https://api.github.com/gists', { + method: 'POST', + headers: { + Authorization: `token ${process.env.GH_TOKEN}`, + }, + body: { + description: 'This is a gist created by ofetch.', + public: true, + files: { + 'unjs.txt': { + content: 'UnJS is awesome!', + } + } + } +}) // Be careful, we use the GitHub API directly. + +console.log(response.url) +``` + +Run the script again: + +```bash +GH_TOKEN=<token> node headers.mjs +``` + +And as a result, we get the URL of the created gist! :raised_hands: + +::alert{type="danger"} +Never put credential token directly in code. Use an environment variable instead. +:: + +### Error handling + +This part is straightforward with [ofetch](https://github.com/unjs/ofetch). Surround the HTTP call with a `try/catch` block and we're done. Let's see how to do it by creating a new file named `error-handling.ts` with the following content: + +```js [error-handling.mjs] +import { ofetch } from 'ofetch' + +try { + await ofetch('https://api.github.com', { + method: 'POST', + }) +} +catch (error) { + // Error will be pretty printed + console.error(error) +} +``` + +With that, we will only see a `FetchError` in the console. We can do better. In fact, we can retrieve the returned error body using `error.data`. Let's update our script a little: + +```js [error-handling.mjs] +// ... + +try { + // ... +} +catch (error) { + console.log(error.data) // This allows us to get the error body. +} +``` + +Then, run the script and we get the following error that we can easily use to display a message to the user: + +```sh +{ + message: 'Not Found', + documentation_url: 'https://docs.github.com/rest/reference/repos#create-a-repository-for-the-authenticated-user' +} +``` + +We can also completely disable error response using the option `ignoreResponseError`. + +## Conclusion + +We have seen how to use [ofetch](https://github.com/unjs/ofetch) to easily make HTTP requests in any environment. We have seen how to use the different options to make more complex requests and how to gracefully handle errors. + +With this 101, we've just scratched the surface of [ofetch](https://github.com/unjs/ofetch). In the next articles, we will use some advanced options like `retry` and `timeout` to make our requests more robust, interceptors to make optimistic updates, and how to create a fetch with some default options! diff --git a/content/4.packages/.template.yml b/content/4.packages/.template.yml index 29e292cb..0fc62c87 100644 --- a/content/4.packages/.template.yml +++ b/content/4.packages/.template.yml @@ -7,4 +7,5 @@ npm: name: npm_name playgrounds: documentation: docs_link +examples: layout: package diff --git a/content/4.packages/ofetch.yml b/content/4.packages/ofetch.yml index 37d0e866..370d5da8 100644 --- a/content/4.packages/ofetch.yml +++ b/content/4.packages/ofetch.yml @@ -6,4 +6,5 @@ github: npm: name: ofetch documentation: https://github.com/unjs/ofetch#ofetch +examples: https://github.com/unjs/ofetch/tree/main/examples layout: package diff --git a/tailwind.config.ts b/tailwind.config.ts index 2527f420..6a955398 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -20,7 +20,7 @@ export default <Partial<Config>> { }, }, fontFamily: { - sans: ['Nunito', 'Nunito fallback' , ...fontFamily.sans], + sans: ['Nunito', 'Nunito fallback', ...fontFamily.sans], }, typography: theme => ({ DEFAULT: {