├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── biome.json ├── bump.config.ts ├── dev ├── .gitignore ├── README.md ├── index.ts ├── package.json ├── server.ts └── tsconfig.json ├── doc ├── .gitignore ├── README.md ├── app │ ├── .DS_Store │ ├── api │ │ ├── .DS_Store │ │ ├── og │ │ │ ├── inter-bold.woff │ │ │ └── route.tsx │ │ └── search │ │ │ └── route.ts │ ├── docs │ │ ├── [[...slug]] │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── global.css │ ├── layout.config.tsx │ ├── layout.tsx │ ├── page.tsx │ └── source.ts ├── components │ ├── .DS_Store │ ├── icons.tsx │ ├── landing │ │ ├── animated-button.tsx │ │ ├── box-reveal.tsx │ │ ├── grid.tsx │ │ ├── ripple.tsx │ │ └── typing-animaton.tsx │ └── ui │ │ └── button.tsx ├── content │ └── docs │ │ ├── authorization.mdx │ │ ├── default-types.mdx │ │ ├── dynamic-parameters.mdx │ │ ├── extra-options.mdx │ │ ├── fetch-options.mdx │ │ ├── fetch-schema.mdx │ │ ├── getting-started.mdx │ │ ├── handling-errors.mdx │ │ ├── hooks.mdx │ │ ├── index.mdx │ │ ├── meta.json │ │ ├── plugins.mdx │ │ ├── timeout-and-retry.mdx │ │ └── utility │ │ └── logger.mdx ├── lib │ ├── better-fetch-options.ts │ ├── example.ts │ ├── metadata.ts │ └── utils.ts ├── mdx-components.tsx ├── next-env.d.ts ├── next.config.mjs ├── package.json ├── postcss.config.js ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── banner.png │ ├── better-fetch-dark.svg │ ├── better-fetch-light.svg │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── tailwind.config.js └── tsconfig.json ├── package.json ├── packages ├── better-fetch │ ├── .npmrc │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── auth.ts │ │ ├── create-fetch │ │ │ ├── index.ts │ │ │ ├── schema.ts │ │ │ └── types.ts │ │ ├── error.ts │ │ ├── fetch.ts │ │ ├── index.ts │ │ ├── plugins.ts │ │ ├── retry.ts │ │ ├── standard-schema.ts │ │ ├── test │ │ │ ├── create.test.ts │ │ │ ├── fetch.test.ts │ │ │ └── test-router.ts │ │ ├── type-utils.ts │ │ ├── types.ts │ │ ├── url.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsup.config.ts │ └── vitest.config.ts └── logger │ ├── .npmrc │ ├── package.json │ ├── src │ ├── index.ts │ └── util.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | merge_group: {} 11 | 12 | jobs: 13 | test: 14 | name: test & typecheck 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v4 19 | 20 | - uses: pnpm/action-setup@v4 21 | name: Install pnpm 22 | with: 23 | version: 9 24 | run_install: false 25 | 26 | - name: Install Node.js 27 | uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | cache: 'pnpm' 31 | 32 | - name: Install dependencies 33 | run: pnpm install 34 | 35 | - name: Build 36 | run: pnpm build 37 | 38 | - name: Typecheck 39 | run: pnpm typecheck 40 | 41 | - name: Test 42 | run: pnpm test 43 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | release: 9 | permissions: 10 | contents: write 11 | id-token: write 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - uses: pnpm/action-setup@v4 19 | name: Install pnpm 20 | with: 21 | version: 9 22 | run_install: false 23 | 24 | - name: Install Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 20 28 | cache: 'pnpm' 29 | 30 | - name: Install dependencies 31 | run: pnpm install 32 | 33 | - name: Check version 34 | id: check_version 35 | run: | 36 | tag=$(echo ${GITHUB_REF#refs/tags/}) 37 | if [[ $tag == *"beta"* ]]; then 38 | echo "::set-output name=tag::beta" 39 | elif [[ $tag == *"alpha"* ]]; then 40 | echo "::set-output name=tag::alpha" 41 | else 42 | echo "::set-output name=tag::latest" 43 | fi 44 | 45 | - name: Build 46 | run: pnpm build 47 | 48 | - run: npx changelogithub 49 | if: steps.check_version.outputs.tag == 'latest' 50 | env: 51 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} 52 | 53 | - name: Publish to npm 54 | run: pnpm -r publish --access public --no-git-checks --tag ${{steps.check_version.outputs.tag}} 55 | env: 56 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .DS_Store 4 | coverage 5 | dist 6 | types 7 | .conf* 8 | .env 9 | .todo.md -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "biome.enabled": true, 3 | "editor.defaultFormatter": "biomejs.biome", 4 | "[typescript]": { 5 | "editor.defaultFormatter": "biomejs.biome" 6 | }, 7 | "[json]": { 8 | "editor.defaultFormatter": "biomejs.biome" 9 | }, 10 | "[github-actions-workflow]": { 11 | "editor.defaultFormatter": "esbenp.prettier-vscode" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Fetch 2 | 3 | Advanced fetch wrapper for typescript with standard schema validations (using zod, valibot, arktype or any other compliant validator), pre-defined routes, callbacks, plugins and more. Works on the browser, node (version 18+), workers, deno and bun. 4 | 5 | # Documentation 6 | 7 | https://better-fetch.vercel.app/ 8 | 9 | ## License 10 | 11 | MIT 12 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.3/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "formatter": { 7 | "enabled": true 8 | }, 9 | "files": { 10 | "ignore": ["dist", "node_modules", ".next"] 11 | }, 12 | "linter": { 13 | "enabled": false 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /bump.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "bumpp"; 2 | import { globSync } from "tinyglobby"; 3 | 4 | export default defineConfig({ 5 | files: globSync(["./packages/*/package.json"], { expandDirectories: false }), 6 | }); 7 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # dev 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.17. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /dev/index.ts: -------------------------------------------------------------------------------- 1 | import { betterFetch, createFetch, createSchema } from "@better-fetch/fetch"; 2 | import { z } from "zod"; 3 | 4 | const $fetch = createFetch({ 5 | schema: createSchema({ 6 | "@patch/user": { 7 | input: z.object({ 8 | id: z.string(), 9 | }), 10 | }, 11 | }), 12 | baseURL: "http://localhost:3000", 13 | onRequest(context) { 14 | console.log("onRequest", JSON.parse(context.body)); 15 | }, 16 | }); 17 | 18 | const f = $fetch("https://jsonplaceholder.typicode.com/todos/:id", { 19 | method: "GET", 20 | params: { 21 | id: "1", 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /dev/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dev", 3 | "private": true, 4 | "module": "index.ts", 5 | "type": "module", 6 | "scripts": { 7 | "client": "bun run index.ts", 8 | "serve": "bun run server.ts" 9 | }, 10 | "devDependencies": { 11 | "@types/bun": "latest" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.0.0" 15 | }, 16 | "dependencies": { 17 | "@better-fetch/fetch": "workspace:*", 18 | "zod": "^3.24.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /dev/server.ts: -------------------------------------------------------------------------------- 1 | Bun.serve({ 2 | fetch(request, server) { 3 | return new Response("Hello World!", { 4 | status: 200, 5 | headers: { 6 | "Content-Type": "text/plain", 7 | }, 8 | }); 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /dev/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /doc/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .map.ts 6 | .contentlayer 7 | .content-collections 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | # Better Fetch Documentation 2 | 3 | A documentation site for [better-fetch](https://github.com/bekacru/better-fetch). 4 | 5 | ## Development 6 | 7 | ```bash 8 | pnpm install 9 | pnpm dev 10 | ``` 11 | 12 | 13 | ## License 14 | 15 | MIT -------------------------------------------------------------------------------- /doc/app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/app/.DS_Store -------------------------------------------------------------------------------- /doc/app/api/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/app/api/.DS_Store -------------------------------------------------------------------------------- /doc/app/api/og/inter-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/app/api/og/inter-bold.woff -------------------------------------------------------------------------------- /doc/app/api/og/route.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/no-unknown-property -- Tailwind CSS `tw` property */ 2 | import { ImageResponse } from "next/og"; 3 | import type { NextRequest } from "next/server"; 4 | 5 | interface Mode { 6 | param: string; 7 | package: string; 8 | name: string; 9 | } 10 | 11 | export const runtime = "edge"; 12 | 13 | const bold = fetch(new URL("./inter-bold.woff", import.meta.url)).then((res) => 14 | res.arrayBuffer(), 15 | ); 16 | 17 | const foreground = "hsl(0 0% 98%)"; 18 | const mutedForeground = "hsl(0 0% 63.9%)"; 19 | const background = "rgba(10, 10, 10)"; 20 | 21 | export async function GET(request: NextRequest): Promise { 22 | const { searchParams } = request.nextUrl; 23 | const title = searchParams.get("title"), 24 | description = searchParams.get("description"); 25 | 26 | return new ImageResponse( 27 | OG({ 28 | title: title ?? "Better Fetch", 29 | description: description ?? "Advanced fetch library for typescript.", 30 | }), 31 | { 32 | width: 1200, 33 | height: 630, 34 | fonts: [{ name: "Inter", data: await bold, weight: 700 }], 35 | }, 36 | ); 37 | } 38 | 39 | function OG({ 40 | title, 41 | description, 42 | }: { 43 | title: string; 44 | description: string; 45 | }): React.ReactElement { 46 | return ( 47 |
54 |
61 |
68 |

{title}

69 |

75 | {description} 76 |

77 |
78 |
79 | 80 |
81 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /doc/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { getPages } from "@/app/source"; 2 | import { createSearchAPI } from "fumadocs-core/search/server"; 3 | 4 | export const { GET } = createSearchAPI("advanced", { 5 | indexes: getPages().map((page) => ({ 6 | title: page.data.title, 7 | structuredData: page.data.exports.structuredData, 8 | id: page.url, 9 | url: page.url, 10 | })), 11 | }); 12 | -------------------------------------------------------------------------------- /doc/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPage, getPages } from "@/app/source"; 2 | import { Card, Cards } from "fumadocs-ui/components/card"; 3 | import { DocsBody, DocsPage } from "fumadocs-ui/page"; 4 | import type { Metadata } from "next"; 5 | import { notFound } from "next/navigation"; 6 | export default async function Page({ 7 | params, 8 | }: { 9 | params: { slug?: string[] }; 10 | }) { 11 | const page = getPage(params.slug); 12 | 13 | if (page == null) { 14 | notFound(); 15 | } 16 | 17 | const MDX = page.data.exports.default; 18 | 19 | return ( 20 | 27 | 28 |

{page.data.title}

29 | 35 |
36 |
37 | ); 38 | } 39 | 40 | export async function generateStaticParams() { 41 | return getPages().map((page) => ({ 42 | slug: page.slugs, 43 | })); 44 | } 45 | 46 | export function generateMetadata({ params }: { params: { slug?: string[] } }) { 47 | const page = getPage(params.slug); 48 | 49 | if (page == null) notFound(); 50 | 51 | return { 52 | title: page.data.title, 53 | description: page.data.description, 54 | } satisfies Metadata; 55 | } 56 | -------------------------------------------------------------------------------- /doc/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 2 | import { DocsLayout } from "fumadocs-ui/layout"; 3 | import Link from "next/link"; 4 | import type { ReactNode } from "react"; 5 | import { docsOptions } from "../layout.config"; 6 | 7 | export default function Layout({ children }: { children: ReactNode }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /doc/app/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | 6 | 7 | @layer base { 8 | :root { 9 | --background: 0 0% 100%; 10 | --foreground: 20 14.3% 4.1%; 11 | --card: 0 0% 100%; 12 | --card-foreground: 20 14.3% 4.1%; 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | --primary: 24.6 95% 53.1%; 16 | --primary-foreground: 60 9.1% 97.8%; 17 | --secondary: 60 4.8% 95.9%; 18 | --secondary-foreground: 24 9.8% 10%; 19 | --muted: 60 4.8% 95.9%; 20 | --muted-foreground: 25 5.3% 44.7%; 21 | --accent: 60 4.8% 95.9%; 22 | --accent-foreground: 24 9.8% 10%; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 60 9.1% 97.8%; 25 | --border: 20 5.9% 90%; 26 | --input: 20 5.9% 90%; 27 | --ring: 24.6 95% 53.1%; 28 | --radius: 0.5rem; 29 | } 30 | 31 | .dark { 32 | --background: 20 14.3% 4.1%; 33 | --foreground: 60 9.1% 97.8%; 34 | --card: 20 14.3% 4.1%; 35 | --card-foreground: 60 9.1% 97.8%; 36 | --popover: 20 14.3% 4.1%; 37 | --popover-foreground: 60 9.1% 97.8%; 38 | --primary: 20.5 90.2% 48.2%; 39 | --primary-foreground: 60 9.1% 97.8%; 40 | --secondary: 12 6.5% 15.1%; 41 | --secondary-foreground: 60 9.1% 97.8%; 42 | --muted: 12 6.5% 15.1%; 43 | --muted-foreground: 24 5.4% 63.9%; 44 | --accent: 12 6.5% 15.1%; 45 | --accent-foreground: 60 9.1% 97.8%; 46 | --destructive: 0 72.2% 50.6%; 47 | --destructive-foreground: 60 9.1% 97.8%; 48 | --border: 12 6.5% 15.1%; 49 | --input: 12 6.5% 15.1%; 50 | --ring: 20.5 90.2% 48.2%; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /doc/app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import { pageTree } from "@/app/source"; 2 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 3 | import { type BaseLayoutProps, type DocsLayoutProps } from "fumadocs-ui/layout"; 4 | import Link from "next/link"; 5 | 6 | // shared configuration 7 | export const baseOptions: BaseLayoutProps = { 8 | nav: { 9 | title: "Better-Fetch", 10 | transparentMode: "top", 11 | }, 12 | links: [], 13 | }; 14 | 15 | // docs layout configuration 16 | export const docsOptions: DocsLayoutProps = { 17 | ...baseOptions, 18 | tree: pageTree, 19 | nav: { 20 | title: "Better Fetch", 21 | }, 22 | sidebar: { 23 | collapsible: false, 24 | footer: ( 25 | 26 | 27 | 28 | ), 29 | defaultOpenLevel: 1, 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /doc/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./global.css"; 2 | import { RootProvider } from "fumadocs-ui/provider"; 3 | import { Inter } from "next/font/google"; 4 | import type { ReactNode } from "react"; 5 | import "fumadocs-ui/twoslash.css"; 6 | import { baseUrl, createMetadata } from "@/lib/metadata"; 7 | 8 | const inter = Inter({ 9 | subsets: ["latin"], 10 | }); 11 | 12 | export const metadata = createMetadata({ 13 | title: { 14 | template: "%s | Better Fetch", 15 | default: "Better Fetch", 16 | }, 17 | description: "Advanced fetch wrapper for typescript.", 18 | metadataBase: baseUrl, 19 | }); 20 | 21 | export default function Layout({ children }: { children: ReactNode }) { 22 | return ( 23 | 24 | 25 | 30 | 36 | 42 | 43 | 44 | 45 | {children} 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /doc/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Icons } from "@/components/icons"; 2 | import Ripple from "@/components/landing/ripple"; 3 | import { Button } from "@/components/ui/button"; 4 | import Link from "next/link"; 5 | 6 | export default function HomePage() { 7 | return ( 8 |
9 | 10 |
11 |

Better Fetch

12 | 13 |

14 | Advanced fetch wrapper for typescript with standard schema validations (using zod, valibot, arktype or any other compliant validator), 15 | pre-defined routes, callbacks, plugins and more. 16 |

17 |
18 | 19 | 23 | 24 | 25 | 29 | 30 |
31 |
32 |
33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /doc/app/source.ts: -------------------------------------------------------------------------------- 1 | //@ts-ignore - this is generated by the build script 2 | import { map } from "@/.map"; 3 | import { loader } from "fumadocs-core/source"; 4 | import { createMDXSource } from "fumadocs-mdx"; 5 | 6 | export const { getPage, getPages, pageTree } = loader({ 7 | baseUrl: "/docs", 8 | rootDir: "docs", 9 | source: createMDXSource(map), 10 | }); 11 | -------------------------------------------------------------------------------- /doc/components/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/components/.DS_Store -------------------------------------------------------------------------------- /doc/components/icons.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function TrafficLightsIcon(props: React.ComponentPropsWithoutRef<"svg">) { 4 | return ( 5 | 10 | ); 11 | } 12 | 13 | export const Icons = { 14 | github: () => ( 15 | 21 | 25 | 26 | ), 27 | docs: () => ( 28 | 34 | 40 | 41 | ), 42 | trafficLights: TrafficLightsIcon, 43 | }; 44 | -------------------------------------------------------------------------------- /doc/components/landing/animated-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { type AnimationProps, motion } from "framer-motion"; 4 | 5 | const animationProps = { 6 | initial: { "--x": "100%", scale: 0.8 }, 7 | animate: { "--x": "-100%", scale: 1 }, 8 | whileTap: { scale: 0.95 }, 9 | transition: { 10 | repeat: Infinity, 11 | repeatType: "loop", 12 | repeatDelay: 1, 13 | type: "spring", 14 | stiffness: 20, 15 | damping: 15, 16 | mass: 2, 17 | scale: { 18 | type: "spring", 19 | stiffness: 200, 20 | damping: 5, 21 | mass: 0.5, 22 | }, 23 | }, 24 | } as AnimationProps; 25 | 26 | const ShinyButton = ({ text = "shiny-button" }) => { 27 | return ( 28 | 32 | 39 | {text} 40 | 41 | 48 | 49 | ); 50 | }; 51 | 52 | export default ShinyButton; 53 | -------------------------------------------------------------------------------- /doc/components/landing/box-reveal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, useAnimation, useInView } from "framer-motion"; 4 | import { useEffect, useRef } from "react"; 5 | 6 | interface BoxRevealProps { 7 | children: JSX.Element; 8 | width?: "fit-content" | "100%"; 9 | boxColor?: string; 10 | duration?: number; 11 | } 12 | 13 | export const BoxReveal = ({ 14 | children, 15 | width = "fit-content", 16 | boxColor, 17 | duration, 18 | }: BoxRevealProps) => { 19 | const mainControls = useAnimation(); 20 | const slideControls = useAnimation(); 21 | 22 | const ref = useRef(null); 23 | const isInView = useInView(ref, { once: true }); 24 | 25 | useEffect(() => { 26 | if (isInView) { 27 | slideControls.start("visible"); 28 | mainControls.start("visible"); 29 | } else { 30 | slideControls.start("hidden"); 31 | mainControls.start("hidden"); 32 | } 33 | }, [isInView, mainControls, slideControls]); 34 | 35 | return ( 36 |
37 | 46 | {children} 47 | 48 | 49 | 67 |
68 | ); 69 | }; 70 | 71 | export default BoxReveal; 72 | -------------------------------------------------------------------------------- /doc/components/landing/grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion } from "framer-motion"; 4 | import { useEffect, useId, useRef, useState } from "react"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | interface GridPatternProps { 9 | width?: number; 10 | height?: number; 11 | x?: number; 12 | y?: number; 13 | strokeDasharray?: any; 14 | numSquares?: number; 15 | className?: string; 16 | maxOpacity?: number; 17 | duration?: number; 18 | repeatDelay?: number; 19 | } 20 | 21 | export function GridPattern({ 22 | width = 40, 23 | height = 40, 24 | x = -1, 25 | y = -1, 26 | strokeDasharray = 0, 27 | numSquares = 50, 28 | className, 29 | maxOpacity = 0.5, 30 | duration = 4, 31 | repeatDelay = 0.5, 32 | ...props 33 | }: GridPatternProps) { 34 | const id = useId(); 35 | const containerRef = useRef(null); 36 | const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); 37 | const [squares, setSquares] = useState(() => generateSquares(numSquares)); 38 | 39 | function getPos() { 40 | return [ 41 | Math.floor((Math.random() * dimensions.width) / width), 42 | Math.floor((Math.random() * dimensions.height) / height), 43 | ]; 44 | } 45 | 46 | // Adjust the generateSquares function to return objects with an id, x, and y 47 | function generateSquares(count: number) { 48 | return Array.from({ length: count }, (_, i) => ({ 49 | id: i, 50 | pos: getPos(), 51 | })); 52 | } 53 | 54 | // Function to update a single square's position 55 | const updateSquarePosition = (id: number) => { 56 | setSquares((currentSquares) => 57 | currentSquares.map((sq) => 58 | sq.id === id 59 | ? { 60 | ...sq, 61 | pos: getPos(), 62 | } 63 | : sq, 64 | ), 65 | ); 66 | }; 67 | 68 | // Update squares to animate in 69 | useEffect(() => { 70 | if (dimensions.width && dimensions.height) { 71 | setSquares(generateSquares(numSquares)); 72 | } 73 | }, [dimensions, numSquares]); 74 | 75 | // Resize observer to update container dimensions 76 | useEffect(() => { 77 | const resizeObserver = new ResizeObserver((entries) => { 78 | for (let entry of entries) { 79 | setDimensions({ 80 | width: entry.contentRect.width, 81 | height: entry.contentRect.height, 82 | }); 83 | } 84 | }); 85 | 86 | if (containerRef.current) { 87 | resizeObserver.observe(containerRef.current); 88 | } 89 | 90 | return () => { 91 | if (containerRef.current) { 92 | resizeObserver.unobserve(containerRef.current); 93 | } 94 | }; 95 | }, [containerRef]); 96 | 97 | return ( 98 | 147 | ); 148 | } 149 | 150 | export default GridPattern; 151 | -------------------------------------------------------------------------------- /doc/components/landing/ripple.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from "react"; 2 | 3 | interface RippleProps { 4 | mainCircleSize?: number; 5 | mainCircleOpacity?: number; 6 | numCircles?: number; 7 | } 8 | 9 | const Ripple = React.memo(function Ripple({ 10 | mainCircleSize = 310, 11 | mainCircleOpacity = 0.2, 12 | numCircles = 20, 13 | }: RippleProps) { 14 | return ( 15 |
16 | {Array.from({ length: numCircles }, (_, i) => { 17 | const size = mainCircleSize + i * 70; 18 | const opacity = mainCircleOpacity - i * 0.03; 19 | const animationDelay = `${i * 0.06}s`; 20 | const borderStyle = i === numCircles - 1 ? "dashed" : "solid"; 21 | const borderOpacity = 5 + i * 5; 22 | 23 | return ( 24 |
42 | ); 43 | })} 44 |
45 | ); 46 | }); 47 | 48 | Ripple.displayName = "Ripple"; 49 | 50 | export default Ripple; 51 | -------------------------------------------------------------------------------- /doc/components/landing/typing-animaton.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useState } from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | interface TypingAnimationProps { 8 | text: string; 9 | duration?: number; 10 | className?: string; 11 | } 12 | 13 | export default function TypingAnimation({ 14 | text, 15 | duration = 200, 16 | className, 17 | }: TypingAnimationProps) { 18 | const [displayedText, setDisplayedText] = useState(""); 19 | const [i, setI] = useState(0); 20 | 21 | useEffect(() => { 22 | const typingEffect = setInterval(() => { 23 | if (i < text.length) { 24 | setDisplayedText(text.substring(0, i + 1)); 25 | setI(i + 1); 26 | } else { 27 | clearInterval(typingEffect); 28 | } 29 | }, duration); 30 | 31 | return () => { 32 | clearInterval(typingEffect); 33 | }; 34 | }, [duration, i]); 35 | 36 | return ( 37 |

43 | {displayedText ? displayedText : text} 44 |

45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /doc/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { type VariantProps, cva } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@/lib/utils"; 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /doc/content/docs/authorization.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Authorization 3 | description: Authorization 4 | --- 5 | 6 | Authorization is a way that allows you to add authentication headers to the request. 7 | Currently, supports `Bearer` and `Basic` authorization. 8 | 9 | ### Bearer 10 | 11 | The bearer authorization is used to add a bearer token to the request. The token is added to the `Authorization` header. 12 | 13 | ```ts twoslash title="fetch.ts" 14 | import { createFetch } from "@better-fetch/fetch"; 15 | 16 | const $fetch = createFetch({ 17 | baseURL: "http://localhost:3000", 18 | auth: { 19 | type: "Bearer", 20 | token: "my-token", 21 | }, 22 | }) 23 | ``` 24 | 25 | You can also pass a function that returns a string. 26 | 27 | ```ts twoslash title="fetch.ts" 28 | import { createFetch } from "@better-fetch/fetch"; 29 | 30 | const authStore = { 31 | getToken: () => "my-token", 32 | } 33 | 34 | //---cut--- 35 | const $fetch = createFetch({ 36 | baseURL: "http://localhost:3000", 37 | auth: { 38 | type: "Bearer", 39 | token: () => authStore.getToken(), 40 | }, 41 | }) 42 | ``` 43 | 44 | 45 | The function will be called only once when the request is made. If it returns undefined, the header will not be added to the request. 46 | 47 | 48 | ### Basic 49 | 50 | The basic authorization is used to add a basic authentication to the request. The username and password are added to the `Authorization` header. 51 | 52 | ```ts twoslash title="fetch.ts" 53 | import { createFetch } from "@better-fetch/fetch"; 54 | 55 | const $fetch = createFetch({ 56 | baseURL: "http://localhost:3000", 57 | auth: { 58 | type: "Basic", 59 | username: "my-username", 60 | password: "my-password", 61 | }, 62 | }) 63 | ``` -------------------------------------------------------------------------------- /doc/content/docs/default-types.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Default Types 3 | description: Default Types 4 | --- 5 | 6 | 7 | ## Default Output 8 | 9 | By default, the response data will always be of type `unknown`. If you want to customize the default type you can pass the `defaultOutput` option to the `createFetch` function. 10 | Note: When you supply a custom output schema using a Standard Schema validator (for example, zod or any alternative), 11 | the provided output schema will override the `defaultOutput` type. This approach offers a strongly typed solution without locking you to a single library. 12 | 13 | 14 | This only serves as a type for the response data it's not used as a validation schema. 15 | 16 | 17 | ```ts twoslash title="fetch.ts" 18 | import { createFetch } from "@better-fetch/fetch"; 19 | // Example using zod (or any Standard Schema compliant library) 20 | import { z } from "zod"; 21 | 22 | const $fetch = createFetch({ 23 | baseURL: "https://jsonplaceholder.typicode.com", 24 | defaultOutput: z.any(), 25 | }) 26 | 27 | const { data, error } = await $fetch("/todos/1") 28 | 29 | // @annotate: Hover over the data object to see the type 30 | ``` 31 | 32 | If you define output schema, the default output type will be ignored. 33 | 34 | ```ts twoslash title="fetch.ts" 35 | import { createFetch } from "@better-fetch/fetch"; 36 | import { z } from "zod"; 37 | 38 | const $fetch = createFetch({ 39 | baseURL: "https://jsonplaceholder.typicode.com", 40 | defaultOutput: z.any(), 41 | }); 42 | 43 | const { data, error } = await $fetch("/todos/1", { 44 | output: z.object({ 45 | userId: z.string(), 46 | id: z.number(), 47 | title: z.string(), 48 | completed: z.boolean(), 49 | }), 50 | }) 51 | // @annotate: Hover over the data object to see the type 52 | ``` 53 | 54 | ## Default error 55 | 56 | The default error type is: 57 | ```ts 58 | { status: number, statusText: string, message?: string } 59 | ``` 60 | 61 | If you want a custom error type, you can pass a `defaultError` option to the `createFetch` function. 62 | Remember: Your custom error type builds on top of the default properties, and if your API returns a JSON error, 63 | // it will be merged with your definition. 64 | 65 | 66 | The `status` and `statusText` properties are always defined. Your custom error definitions are only 67 | inferred if the API returns a JSON error object. 68 | 69 | 70 | ```ts twoslash title="fetch.ts" 71 | import { createFetch } from "@better-fetch/fetch"; 72 | import { z } from "zod"; // Example only 73 | 74 | const $fetch = createFetch({ 75 | baseURL: "https://jsonplaceholder.typicode.com", 76 | defaultError: z.object({ 77 | message: z.string().optional(), 78 | error: z.string(), 79 | }), 80 | }) 81 | 82 | const { data, error } = await $fetch("/todos/1") 83 | // @annotate: Hover over the error object to see the type 84 | ``` 85 | 86 | -------------------------------------------------------------------------------- /doc/content/docs/dynamic-parameters.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Dynamic Parameters 3 | description: Dynamic Parameters 4 | --- 5 | 6 | Dynamic parameters are parameters that are defined in the url path. They are defined using the `:` prefix. When the request is made, the dynamic parameters will be replaced with the values passed in the `params` option. 7 | 8 | ```ts twoslash title="fetch.ts" 9 | import { createFetch } from "@better-fetch/fetch"; 10 | 11 | const $fetch = createFetch({ 12 | baseURL: "http://localhost:3000", 13 | }) 14 | 15 | const res = await $fetch("/path/:id", { 16 | params: { 17 | id: "1" 18 | } 19 | }) 20 | 21 | const res2 = await $fetch("/repos/:owner/:repo", { 22 | params: { 23 | owner: "octocat", 24 | repo: "hello-world" 25 | } 26 | }) 27 | ``` 28 | -------------------------------------------------------------------------------- /doc/content/docs/extra-options.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Extra Options 3 | description: Fetch Options 4 | --- 5 | 6 | These are better fetch specefic options. 7 | 8 | ### Better Fetch Options 9 | 10 | -------------------------------------------------------------------------------- /doc/content/docs/fetch-options.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: All Options 3 | description: Fetch Options 4 | --- 5 | 6 | Fetch options is a union of `fetch` options and additional better fetch options. 7 | 8 | ### All Fetch Options 9 | -------------------------------------------------------------------------------- /doc/content/docs/fetch-schema.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fetch Schema 3 | description: Fetch Schema 4 | --- 5 | 6 | Fetch schema allows you to pre-define the URL path and the shape of request and response data. You can easily document your API using this schema. 7 | 8 | Better Fetch now uses Standard Schema internally, allowing you to bring your own Standard Schema-compliant validator (e.g., Zod, Valibot, ArkType). 9 | 10 | To create a fetch schema, you need to import the `createSchema` function from `@better-fetch/fetch`. 11 | 12 | ```package-install 13 | npm i zod 14 | ``` 15 | 16 | To create a fetch schema, you need to import the `createSchema` function from `@better-fetch/fetch`. 17 | 18 | ```ts twoslash title="fetch.ts" 19 | import { createSchema, createFetch } from "@better-fetch/fetch"; 20 | import { z } from "zod"; 21 | 22 | export const schema = createSchema({ // [!code highlight] 23 | "/path": { // [!code highlight] 24 | input: z.object({ // [!code highlight] 25 | userId: z.string(), // [!code highlight] 26 | id: z.number(), // [!code highlight] 27 | title: z.string(), // [!code highlight] 28 | completed: z.boolean(), // [!code highlight] 29 | }), // [!code highlight] 30 | output: z.object({ // [!code highlight] 31 | userId: z.string(), // [!code highlight] 32 | id: z.number(), // [!code highlight] 33 | title: z.string(), // [!code highlight] 34 | completed: z.boolean(), // [!code highlight] 35 | }), // [!code highlight] 36 | } // [!code highlight] 37 | }) // [!code highlight] 38 | 39 | const $fetch = createFetch({ 40 | baseURL: "https://jsonplaceholder.typicode.com", 41 | schema: schema // [!code highlight] 42 | }); 43 | 44 | ``` 45 | 46 | 47 | ## Fetch Schema 48 | 49 | The Fetch Schema is a map of path/url and schema. The path is the url path and the schema is an object with 50 | `input`, `output`, `query` and `params` keys. 51 | 52 | The `input` key is the schema of the request data. The `output` key is the schema of the response data. The `query` key is the schema of the query params. The `params` key is dynamic path parameters. 53 | 54 | ### Input 55 | 56 | The input schema is the schema of the request data. The `input` key is the schema of the request data. If you defined an input schema, the data will be required to be passed as a body of the request. 57 | 58 | 59 | If you define an input schema, a `post` method will be used to make the request and if there is no input schema, a `get` method will be used. See [method modifiers](#method-modifiers) section for defining specific methods. 60 | 61 | 62 | ```ts twoslash title="fetch.ts" 63 | import { createFetch, createSchema } from "@better-fetch/fetch"; 64 | import { z } from "zod"; 65 | 66 | const $fetch = createFetch({ 67 | baseURL: "https://jsonplaceholder.typicode.com", 68 | schema: createSchema({ 69 | "/path": { 70 | input: z.object({ 71 | userId: z.string(), 72 | id: z.number(), 73 | title: z.string(), 74 | completed: z.boolean(), 75 | }), 76 | }, 77 | }), 78 | }) 79 | 80 | // @errors: 2739 81 | const { data, error } = await $fetch("/path", { 82 | body: {} 83 | }) 84 | ``` 85 | 86 | To make the body optional you can wrap the schema with `z.optional`. 87 | 88 | 89 | ### Output 90 | 91 | The `output` key is the schema of the response data. If you defined an output schema, the data will be returned as the response body. 92 | 93 | 94 | ```ts twoslash title="fetch.ts" 95 | import { createFetch, createSchema } from "@better-fetch/fetch"; 96 | import { z } from "zod"; 97 | 98 | const $fetch = createFetch({ 99 | baseURL: "https://jsonplaceholder.typicode.com", 100 | schema: createSchema({ 101 | "/path": { 102 | output: z.object({ 103 | userId: z.string(), 104 | id: z.number(), 105 | title: z.string(), 106 | completed: z.boolean(), 107 | }), 108 | }, 109 | }), 110 | }) 111 | 112 | const { data, error } = await $fetch("/path") 113 | // @annotate: Hover over the data object to see the type 114 | ``` 115 | 116 | 117 | ### Query 118 | 119 | The query schema is the schema of the query params. The `query` key is the schema of the query params. If you defined a query schema, the data will be passed as the query params. 120 | 121 | ```ts twoslash title="fetch.ts" 122 | import { createFetch, createSchema } from "@better-fetch/fetch"; 123 | import { z } from "zod"; 124 | 125 | const $fetch = createFetch({ 126 | baseURL: "https://jsonplaceholder.typicode.com", 127 | schema: createSchema({ 128 | "/path": { 129 | query: z.object({ 130 | userId: z.string(), 131 | id: z.number(), 132 | title: z.string(), 133 | completed: z.boolean(), 134 | }), 135 | }, 136 | }), 137 | }) 138 | 139 | // @errors: 2739 140 | const { data, error } = await $fetch("/path", { 141 | query: {} 142 | }) 143 | // @annotate: Hover over the data object to see the type 144 | ``` 145 | 146 | ### Dynamic Path Parameters 147 | 148 | The params schema is the schema of the path params. You can either use the `params` key to define the paramters or prepend `:` to the path to make the parameters dynamic. 149 | 150 | 151 | If you define more than one dynamic path parameter using the string modifier the paramters will be required to be passed as an array of values in the order they are defined. 152 | 153 | 154 | ```ts twoslash title="fetch.ts" 155 | import { createFetch, createSchema } from "@better-fetch/fetch"; 156 | import { z } from "zod"; 157 | 158 | const schema = createSchema({ 159 | "/user/:id": { 160 | output: z.object({ 161 | name: z.string(), 162 | }), 163 | }, 164 | "/post": { 165 | params: z.object({ 166 | id: z.string(), 167 | title: z.string(), 168 | }), 169 | }, 170 | "/post/:id/:title": { 171 | output: z.object({ 172 | title: z.string(), 173 | }), 174 | } 175 | }) 176 | 177 | 178 | const $fetch = createFetch({ 179 | baseURL: "https://jsonplaceholder.typicode.com", 180 | schema: schema 181 | }) 182 | 183 | const response1 = await $fetch("/user/:id", { 184 | params: { 185 | id: "1", 186 | } 187 | }) 188 | 189 | const response2 = await $fetch("/post", { 190 | params: { 191 | id: "1", 192 | title: "title" 193 | }, 194 | }) 195 | 196 | const response3 = await $fetch("/post/:id/:title", { 197 | params: { 198 | id: "1", 199 | title: "title" 200 | } 201 | }) 202 | 203 | ``` 204 | 205 | 206 | ### Method Modifiers 207 | 208 | By default the `get` and `post` methods are used to make the request based on whether the input schema is defined or not. You can use the `method` modifier to define the method to be used. 209 | 210 | The method modifiers are `@get`, `@post`, `@put`, `@patch`, `@delete` and `@head`. You prepend the method name to the path to define the method. 211 | 212 | ```ts twoslash title="fetch.ts" 213 | import { createFetch, createSchema } from "@better-fetch/fetch"; 214 | import { z } from "zod"; 215 | 216 | const $fetch = createFetch({ 217 | baseURL: "https://jsonplaceholder.typicode.com", 218 | schema: createSchema({ 219 | "@put/user": { // [!code highlight] 220 | input: z.object({ 221 | title: z.string(), 222 | completed: z.boolean(), 223 | }), 224 | output: z.object({ 225 | title: z.string(), 226 | completed: z.boolean(), 227 | }), 228 | }, 229 | }), 230 | }) 231 | 232 | const { data, error } = await $fetch("/@put/user", { 233 | body: { 234 | title: "title", 235 | completed: true, 236 | } 237 | }) 238 | // @annotate: the request will be made to "/user" path with a PUT method. 239 | ``` 240 | 241 | 242 | ## Strict Schema 243 | 244 | By default if you define schema better fetch still allows you to make a call to other routes that's not defined on the schema. If you want to enforce only the keys defined to be inferred as valid you can use pass the `strict` option to the schema. 245 | 246 | ```ts twoslash title="fetch.ts" 247 | import { createFetch, createSchema } from "@better-fetch/fetch"; 248 | import { z } from "zod"; 249 | 250 | const $fetch = createFetch({ 251 | baseURL: "https://jsonplaceholder.typicode.com", 252 | schema: createSchema({ 253 | "/path": { 254 | output: z.object({ 255 | userId: z.string(), 256 | id: z.number(), 257 | title: z.string(), 258 | completed: z.boolean(), 259 | }), 260 | }, 261 | }, 262 | { // [!code highlight] 263 | strict: true // [!code highlight] 264 | }), // [!code highlight] 265 | }) 266 | // @errors: 2345 267 | const { data, error } = await $fetch("/invalid-path") 268 | ``` 269 | 270 | 271 | -------------------------------------------------------------------------------- /doc/content/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Getting started with Better Fetch 4 | --- 5 | 6 | ### Installation 7 | 8 | ```package-install 9 | npm i @better-fetch/fetch 10 | ``` 11 | 12 | If you plan to use runtime validation, you need to install [standard schema](https://github.com/standard-schema/standard-schema) compliant validator like [zod](https://github.com/colinhacks/zod), [valibot](https://valibot.dev), [arktype](https://github.com/arktypeio/arktype) and so on. 13 | 14 | ```package-install 15 | npm i zod # valibot, arktype... 16 | ``` 17 | 18 | ### Quick Start 19 | 20 | The fastest way to start using Better Fetch is to import the `betterFetch` function. 21 | You can define the response type using generics or use a schema that supports Standard Schema (recommended). 22 | 23 | ```ts twoslash title="fetch.ts" 24 | import { betterFetch } from '@better-fetch/fetch'; 25 | 26 | // Using generic type 27 | const { data, error } = await betterFetch<{ 28 | userId: string; 29 | id: number; 30 | title: string; 31 | completed: boolean; 32 | }>("https://jsonplaceholder.typicode.com/todos/1"); 33 | 34 | 35 | // Using a Standard Schema validator (for example, zod) 36 | import { z } from 'zod'; // or your preferred Standard Schema compliant library 37 | 38 | const { data: todos, error: todoError } = await betterFetch("https://jsonplaceholder.typicode.com/todos/1", { 39 | output: z.object({ 40 | userId: z.string(), 41 | id: z.number(), 42 | title: z.string(), 43 | completed: z.boolean(), 44 | }) 45 | }); 46 | // @annotate: Hover over the data object to see the type 47 | ``` 48 | 49 | 50 | Make sure `strict` mode is enabled in your tsconfig when using schema validations. 51 | 52 | 53 | Better fetch by default returns a `Promise` that resolves to an object of `data` and `error` but if you pass the `throw` option, it will return the parsed response data only. 54 | 55 | ### Create Fetch 56 | 57 | Create Fetch allows you to create a better fetch instance with custom configurations. 58 | 59 | ```ts twoslash title="fetch.ts" 60 | import { createFetch } from "@better-fetch/fetch"; 61 | 62 | export const $fetch = createFetch({ 63 | baseURL: "https://jsonplaceholder.typicode.com", 64 | retry: { 65 | type: "linear", 66 | attempts: 3, 67 | delay: 1000 68 | } 69 | }); 70 | 71 | const { data, error } = await $fetch<{ 72 | userId: number; 73 | id: number; 74 | title: string; 75 | completed: boolean; 76 | }>("/todos/1"); 77 | ``` 78 | You can pass more options see the [Fetch Options](/docs/fetch-options) section for more details. 79 | 80 | ### Throwing Errors 81 | 82 | You can throw errors instead of returning them by passing the `throw` option. 83 | 84 | If you pass the `throw` option, the `betterFetch` function will throw an error. And instead of returning `data` and `error` object it'll only the response data as it is. 85 | 86 | ```ts twoslash title="fetch.ts" 87 | import { createFetch } from '@better-fetch/fetch'; 88 | import { z } from 'zod'; 89 | 90 | const $fetch = createFetch({ 91 | baseURL: "https://jsonplaceholder.typicode.com", 92 | throw: true, 93 | }); 94 | 95 | const data = await $fetch<{ 96 | userId: number; 97 | }>("https://jsonplaceholder.typicode.com/todos/1"); 98 | ``` 99 | Learn more about handling errors [Handling Errors](/docs/handling-errors) section. 100 | 101 | ### Fetch Schema 102 | 103 | Fetch schema enables you to pre-define the URL path and the shape of request and response data. This makes it easy to document your API. 104 | 105 | 106 | Plugins can also define fetch schemas. See [Plugins](/docs/plugins) section for more details. 107 | 108 | 109 | The output of the schema will be validated using your schema and if the validation fails, it'll throw an error. 110 | 111 | ```ts twoslash title="fetch.ts" 112 | import { createSchema, createFetch } from "@better-fetch/fetch"; 113 | 114 | // ZOD example 115 | import { z } from "zod"; 116 | 117 | export const zodSchema = createSchema({ // [!code highlight] 118 | "/path": { // [!code highlight] 119 | input: z.object({ // [!code highlight] 120 | userId: z.string(), // [!code highlight] 121 | id: z.number(), // [!code highlight] 122 | title: z.string(), // [!code highlight] 123 | completed: z.boolean(), // [!code highlight] 124 | }), // [!code highlight] 125 | output: z.object({ // [!code highlight] 126 | userId: z.string(), // [!code highlight] 127 | id: z.number(), // [!code highlight] 128 | title: z.string(), // [!code highlight] 129 | completed: z.boolean(), // [!code highlight] 130 | }), // [!code highlight] 131 | } // [!code highlight] 132 | }) // [!code highlight] 133 | 134 | const $fetch = createFetch({ 135 | baseURL: "https://jsonplaceholder.typicode.com", 136 | schema: zodSchema // [!code highlight] 137 | }); 138 | 139 | const { data, error } = await $fetch("/path", { 140 | body: { 141 | userId: "1", 142 | id: 1, 143 | title: "title", 144 | completed: true, 145 | }, 146 | }); 147 | ``` 148 | 149 | Learn more about fetch schema [Fetch Schema](/docs/fetch-schema) section. -------------------------------------------------------------------------------- /doc/content/docs/handling-errors.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Handling Errors 3 | description: Handling Errors 4 | --- 5 | 6 | ## Default Error Type 7 | Better fetch by default returns response errors as a value. By defaullt, the error object has 3 properties `status`, `statusText` and `message` properties. 8 | 9 | `status` and `statusText` are always defined. If the api returns a json error object it will be parsed and returned with the error object. By default `error` includes `message` property that can be string or undefined. 10 | 11 | ```ts twoslash title="fetch.ts" 12 | import { betterFetch } from '@better-fetch/fetch'; 13 | import { z } from 'zod'; 14 | 15 | const { error } = await betterFetch("https://jsonplaceholder.typicode.com/todos/1"); 16 | // @annotate: Hover over the error object to see the type 17 | ``` 18 | ## Custom Error Type 19 | You can pass a custom error type to be inferred as a second generic argument. 20 | 21 | ```ts 22 | import { betterFetch } from 'better-fetch'; 23 | 24 | const { error } = await betterFetch<{ 25 | id: number; 26 | userId: string; 27 | title: string; 28 | completed: boolean; 29 | }, 30 | { 31 | message?: string; // [!code highlight] 32 | error?: string;// [!code highlight] 33 | }>("https://jsonplaceholder.typicode.com/todos/1"); 34 | ``` 35 | 36 | 37 | If you pass a custom error type, it will override the default error type except for the status and statusText properties. If you still need the message property, you need to include it in your custom error type. 38 | 39 | 40 | ## Throwing Errors 41 | 42 | If you prefer to throw errors instead of returning them, you can use the `throw` option. 43 | 44 | When you pass the `throw` option, the `betterFetch` function will throw an error. And instead of returning `data` and `error` object it'll only the response data as it is. 45 | 46 | ```ts twoslash title="fetch.ts" 47 | import { betterFetch } from '@better-fetch/fetch'; 48 | import { z } from 'zod'; 49 | 50 | const data = await betterFetch("https://jsonplaceholder.typicode.com/todos/1", { 51 | throw: true, // [!code highlight] 52 | output: z.object({ 53 | userId: z.string(), 54 | id: z.number(), 55 | title: z.string(), 56 | completed: z.boolean(), 57 | }), 58 | }); 59 | 60 | ``` 61 | 62 | ## Inferring Response When Using Generics and `throw` Option 63 | 64 | When you pass the `throw` option to the `betterFetch` function, it will throw an error instead of returning it. This means the error will not be returned as a value. However, if you specify the response type as a generic, the `error` object will still be returned, and `data` will be inferred as possibly `null` or the specified type. This issue arises because the `throw` option cannot be inferred when a generic value is passed, due to a TypeScript limitation. 65 | 66 | To address this, you have two options. If you use either option, the `error` object will no longer exist, and the response type will be inferred correctly without being unioned with `null`. 67 | 68 | 1. Create a custom fetch instance with the `throw` option. 69 | 70 | ```ts twoslash title="fetch.ts" 71 | import { createFetch } from "@better-fetch/fetch"; 72 | 73 | export const $fetch = createFetch({ 74 | baseURL: "https://jsonplaceholder.typicode.com", 75 | retry: 2, 76 | throw: true, 77 | }); 78 | 79 | 80 | const data = await $fetch<{ 81 | userId: number; 82 | id: number; 83 | title: string; 84 | completed: boolean; 85 | }>("/todos/1"); 86 | ``` 87 | 88 | 2. Pass false as a second generic argument to the `betterFetch` function. 89 | 90 | ```ts twoslash title="fetch.ts" 91 | import { betterFetch } from '@better-fetch/fetch'; 92 | import { z } from 'zod'; 93 | 94 | const data = await betterFetch<{ 95 | userId: number; 96 | id: number; 97 | title: string; 98 | completed: boolean; 99 | }, 100 | false // [!code highlight] 101 | >("https://jsonplaceholder.typicode.com/todos/1"); 102 | ``` -------------------------------------------------------------------------------- /doc/content/docs/hooks.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hooks 3 | description: Hooks 4 | --- 5 | 6 | Hooks are functions that are called at different stages of the request lifecycle. 7 | 8 | ```ts twoslash title="fetch.ts" 9 | import { createFetch } from "@better-fetch/fetch"; 10 | 11 | const $fetch = createFetch({ 12 | baseURL: "http://localhost:3000", 13 | onRequest(context) { 14 | return context; 15 | }, 16 | onResponse(context) { 17 | return context.response 18 | }, 19 | onError(context) { 20 | }, 21 | onSuccess(context) { 22 | }, 23 | }) 24 | 25 | ``` 26 | 27 | ## On Request 28 | 29 | a callback function that will be called when a request is about to be made. The function will be called with the request context as an argument and it's expected to return the modified request context. 30 | 31 | ```ts twoslash title="fetch.ts" 32 | import { createFetch } from "@better-fetch/fetch"; 33 | 34 | const $fetch = createFetch({ 35 | baseURL: "http://localhost:3000", 36 | onRequest(context) { 37 | // do something with the context 38 | return context; 39 | }, 40 | }) 41 | ``` 42 | 43 | ## On Response 44 | 45 | a callback function that will be called when a response is received. The function will be called with the response context which includes the `response` and the `requestContext` as an argument and it's expected to return response. 46 | 47 | 48 | ```ts twoslash title="fetch.ts" 49 | import { createFetch } from "@better-fetch/fetch"; 50 | 51 | const $fetch = createFetch({ 52 | baseURL: "http://localhost:3000", 53 | onResponse(context) { 54 | // do something with the context 55 | return context.response // return the response 56 | }, 57 | }) 58 | ``` 59 | 60 | 61 | ## On Success and On Error 62 | 63 | on success and on error are callbacks that will be called when a request is successful or when an error occurs. The function will be called with the response context as an argument and it's not expeceted to return anything. 64 | 65 | ```ts twoslash title="fetch.ts" 66 | import { createFetch } from "@better-fetch/fetch"; 67 | 68 | const $fetch = createFetch({ 69 | baseURL: "http://localhost:3000", 70 | onSuccess(context) { 71 | // do something with the context 72 | }, 73 | onError(context) { 74 | // do something with the context 75 | }, 76 | }) 77 | ``` -------------------------------------------------------------------------------- /doc/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Getting started with Better Fetch 4 | --- 5 | 6 | # Better Fetch 7 | 8 | Better Fetch is an advanced fetch wrapper for TypeScript that supports any Standard Schema-compliant validator (like Zod, Valibot, ArkType, etc) for runtime validation and type inference. It features error-as-value handling, pre-defined routes, hooks, plugins and more. Works on the browser, node (version 18+), workers, deno and bun. 9 | 10 | ## Features 11 | 12 | 16 | 20 | 24 | 29 | 34 | 39 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /doc/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "title:": "guide", 3 | "root": true, 4 | "pages": [ 5 | "---Guide---", 6 | "index", 7 | "getting-started", 8 | "handling-errors", 9 | "---Helpers---", 10 | "authorization", 11 | "dynamic-parameters", 12 | "timeout-and-retry", 13 | "---Advanced---", 14 | "fetch-schema", 15 | "default-types", 16 | "hooks", 17 | "plugins", 18 | "---Plugins---", 19 | "utility", 20 | "---Reference---", 21 | "fetch-options", 22 | "extra-options" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /doc/content/docs/plugins.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Plugins 3 | description: Plugins 4 | --- 5 | 6 | Plugins are functions that can be used to modify the request, response, error and other parts of the request lifecycle and can be used to define Fetch Schema. 7 | 8 | 9 | ### Init 10 | 11 | The init function is called before the request is made and any of the internal functions are called. It takes the `url` and `options` as arguments and is expected to return the modified `url` and `options`. 12 | 13 | ```ts twoslash title="fetch.ts" 14 | import { createFetch, BetterFetchPlugin } from "@better-fetch/fetch"; 15 | 16 | const myPlugin = { 17 | id: "my-plugin", 18 | name: "My Plugin", 19 | init: async (url, options) => { 20 | if(url.startsWith("http://")) { 21 | const _url = new URL(url) 22 | const DEV_URL = "http://localhost:3000" 23 | return { 24 | url: `${DEV_URL}/${_url.pathname}`, 25 | options, 26 | } 27 | } 28 | return { 29 | url, 30 | options, 31 | } 32 | }, 33 | } satisfies BetterFetchPlugin; 34 | 35 | const $fetch = createFetch({ 36 | baseURL: "https://jsonplaceholder.typicode.com", 37 | plugins: [myPlugin], 38 | }); 39 | ``` 40 | 41 | ### Hooks 42 | 43 | Hooks are functions that are called at different stages of the request lifecycle. See [Hooks](/docs/hooks) for more information. 44 | 45 | 46 | ```ts twoslash title="fetch.ts" 47 | import { createFetch, BetterFetchPlugin } from "@better-fetch/fetch"; 48 | 49 | const myPlugin = { 50 | id: "my-plugin", 51 | name: "My Plugin", 52 | hooks: { 53 | onRequest(context) { 54 | // do something with the context 55 | return context; 56 | }, 57 | onResponse(context) { 58 | // do something with the context 59 | return context; 60 | }, 61 | onError(context) { 62 | // do something with the context 63 | }, 64 | onSuccess(context) { 65 | // do something with the context 66 | }, 67 | } 68 | } satisfies BetterFetchPlugin; 69 | 70 | const $fetch = createFetch({ 71 | baseURL: "https://jsonplaceholder.typicode.com", 72 | plugins: [myPlugin], 73 | }); 74 | ``` 75 | 76 | 77 | If more than one plugin is registered, the hooks will be called in the order they are registered. 78 | 79 | 80 | ### Schema 81 | 82 | You can define a schema for a plugin. This allows you to easily document the API usage using a schema. 83 | The schema is now based on the Standard Schema specification. 84 | 85 | Note: Better Fetch now uses Standard Schema internally so you can bring your own Standard Schema–compliant validator. You are no longer limited to zod. 86 | 87 | ```ts twoslash title="fetch.ts" 88 | import { createFetch, createSchema, BetterFetchPlugin } from "@better-fetch/fetch"; 89 | // Example using zod (or any Standard Schema compliant validator) 90 | import { z } from "zod"; 91 | // @errors: 2353 2561 92 | const plugin = { 93 | id: "my-plugin", 94 | name: "My Plugin", 95 | schema: createSchema({ 96 | "/path": { 97 | input: z.object({ 98 | /** 99 | * You can write descriptions for the properties. Hover over the property to see 100 | * the description. 101 | */ 102 | userId: z.string(), 103 | /** 104 | * The id property is required 105 | */ 106 | id: z.number(), 107 | }), 108 | output: z.object({ 109 | title: z.string(), 110 | completed: z.boolean(), 111 | }), 112 | } 113 | },{ 114 | baseURL: "https://jsonplaceholder.typicode.com", 115 | }) 116 | } satisfies BetterFetchPlugin; 117 | 118 | const $fetch = createFetch({ 119 | baseURL: "localhost:3000" 120 | }) 121 | 122 | const { data, error } = await $fetch("https://jsonplaceholder.typicode.com/path", { 123 | body: { 124 | userId: "1", 125 | id: 1, 126 | title: "title", 127 | completed: true, 128 | }, 129 | }); 130 | 131 | //@annotate: baseURL is inferred to "https://jsonplaceholder.typicode.com" 132 | ``` 133 | 134 | You can also pass a `prefix` to the `createSchema` function to prefix all the routes. 135 | 136 | 137 | ### Get options 138 | 139 | The `getOptions` function allows you to define additional options that can be passed to the fetch function. This is useful when you want to pass options to the plugins that are not part of the `BetterFetchPlugin` interface. 140 | 141 | ```ts twoslash title="fetch.ts" 142 | import { createFetch, createSchema, BetterFetchPlugin } from "@better-fetch/fetch"; 143 | import { z } from "zod"; 144 | 145 | const plugin = { 146 | id: "my-plugin", 147 | name: "My Plugin", 148 | getOptions() { 149 | return z.object({ 150 | onUploadProgress: z.function().args(z.object({ 151 | loaded: z.number(), 152 | total: z.number(), 153 | })), 154 | }); 155 | }, 156 | } satisfies BetterFetchPlugin; 157 | 158 | const $fetch = createFetch({ 159 | baseURL: "https://jsonplaceholder.typicode.com", 160 | plugins: [plugin], 161 | }); 162 | 163 | const { data, error } = await $fetch("https://jsonplaceholder.typicode.com/path", { 164 | onUploadProgress({ 165 | loaded, 166 | total, 167 | }) { 168 | console.log(`Uploaded ${loaded} of ${total} bytes`); 169 | }, 170 | }); 171 | ``` 172 | 173 | ### Properties 174 | 175 | -------------------------------------------------------------------------------- /doc/content/docs/timeout-and-retry.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Timeout and Retry 3 | description: Timeout and Retry 4 | --- 5 | 6 | 7 | Timeout and retry are two options that can be used to control the request timeout and retry behavior. 8 | 9 | 10 | ## Timeout 11 | 12 | You can set the timeout in milliseconds. 13 | 14 | ```ts twoslash title="fetch.ts" 15 | import { createFetch } from "@better-fetch/fetch"; 16 | 17 | const $fetch = createFetch({ 18 | baseURL: "http://localhost:3000", 19 | timeout: 5000, 20 | }) 21 | // ---cut--- 22 | const res = await $fetch("/api/users", { 23 | timeout: 10000, 24 | }); 25 | ``` 26 | 27 | 28 | ## Auto Retry 29 | 30 | You can set the retry count and interval in milliseconds. 31 | 32 | ```ts twoslash title="fetch.ts" 33 | import { createFetch } from "@better-fetch/fetch"; 34 | 35 | const $fetch = createFetch({ 36 | baseURL: "http://localhost:3000", 37 | }) 38 | // ---cut--- 39 | const res = await $fetch("/api/users", { 40 | retry: 3 41 | }); 42 | ``` 43 | 44 | ## Advanced Retry Options 45 | Better fetch provides flexible retry mechanisms with both linear and exponential backoff strategies. You can customize the retry behavior to suit your specific needs. 46 | 47 | Basic retry with number of attempts: 48 | ```ts twoslash title="fetch.ts" 49 | import { createFetch } from "@better-fetch/fetch"; 50 | 51 | const $fetch = createFetch({ 52 | baseURL: "http://localhost:3000", 53 | }) 54 | // ---cut--- 55 | const res = await $fetch("https://jsonplaceholder.typicode.com/todos/1", { 56 | retry: 3 57 | }); 58 | ``` 59 | 60 | Linear retry strategy: 61 | ```ts twoslash title="fetch.ts" 62 | import { createFetch } from "@better-fetch/fetch"; 63 | 64 | const $fetch = createFetch({ 65 | baseURL: "http://localhost:3000", 66 | }) 67 | // ---cut--- 68 | const res = await $fetch("https://jsonplaceholder.typicode.com/todos/1", { 69 | retry: { 70 | type: "linear", 71 | attempts: 3, 72 | delay: 1000 // 1 second delay between each attempt 73 | } 74 | }); 75 | ``` 76 | 77 | Exponential backoff strategy: 78 | ```ts twoslash title="fetch.ts" 79 | import { createFetch } from "@better-fetch/fetch"; 80 | 81 | const $fetch = createFetch({ 82 | baseURL: "http://localhost:3000", 83 | }) 84 | // ---cut--- 85 | const res = await $fetch("https://jsonplaceholder.typicode.com/todos/1", { 86 | retry: { 87 | count: 3, 88 | interval: 1000, //optional 89 | type: "exponential", 90 | attempts: 5, 91 | baseDelay: 1000, // Start with 1 second delay 92 | maxDelay: 10000 // Cap the delay at 10 seconds, so requests would go out after 1s then 2s, 4s, 8s, 10s 93 | } 94 | }); 95 | ``` 96 | 97 | Custom retry condition: 98 | ```ts twoslash title="fetch.ts" 99 | import { createFetch } from "@better-fetch/fetch"; 100 | 101 | const $fetch = createFetch({ 102 | baseURL: "http://localhost:3000", 103 | }) 104 | // ---cut--- 105 | const res = await $fetch("https://jsonplaceholder.typicode.com/todos/1", { 106 | retry: { 107 | type: "linear", 108 | attempts: 3, 109 | delay: 1000, 110 | shouldRetry: (response) => { 111 | if(response === null) return true; 112 | if(response.status === 429) return true; 113 | if(response.status !== 200) return true; 114 | return response.json().then( 115 | data => data.completed === false 116 | ).catch( 117 | err => true 118 | ) 119 | } 120 | } 121 | }); 122 | ``` 123 | 124 | Retry with callback: 125 | ```ts twoslash title="fetch.ts" 126 | import { createFetch } from "@better-fetch/fetch"; 127 | 128 | const $fetch = createFetch({ 129 | baseURL: "http://localhost:3000", 130 | }) 131 | // ---cut--- 132 | const res = await $fetch("https://jsonplaceholder.typicode.com/todos/1", { 133 | retry: 3, 134 | onRetry: (response) => { 135 | console.log(`Retrying request.`); 136 | } 137 | }); 138 | ``` -------------------------------------------------------------------------------- /doc/content/docs/utility/logger.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Logger 3 | description: Logger 4 | --- 5 | 6 | The logger plugin logs information on request, error or success and on retry. 7 | 8 | ```package-install 9 | npm i @better-fetch/logger 10 | ``` 11 | 12 | ## Usage 13 | 14 | ```ts twoslash title="fetch.ts" 15 | import { createFetch } from "@better-fetch/fetch"; 16 | import { logger } from "@better-fetch/logger"; 17 | 18 | const $fetch = createFetch({ 19 | baseURL: "http://localhost:3000", 20 | plugins: [ 21 | logger(), 22 | ], 23 | }); 24 | ``` 25 | 26 | ## Options 27 | 28 | ### `enabled` 29 | 30 | Enable or disable the logger. 31 | 32 | ```ts title="fetch.ts" 33 | import { createFetch } from "@better-fetch/fetch"; 34 | import { logger } from "@better-fetch/logger"; 35 | 36 | const $fetch = createFetch({ 37 | baseURL: "http://localhost:3000", 38 | plugins: [ 39 | logger({ 40 | enabled: process.env.NODE_ENV === "development", 41 | }), 42 | ], 43 | }); 44 | ``` 45 | 46 | ### `console` 47 | 48 | By default the logger plugin uses the consola package to log the requests and responses. You can pass a custom console object to the logger plugin to use a different logger. 49 | 50 | ```ts title="fetch.ts" 51 | import { createFetch } from "@better-fetch/fetch"; 52 | import { logger } from "@better-fetch/logger"; 53 | 54 | const $fetch = createFetch({ 55 | baseURL: "http://localhost:3000", 56 | plugins: [ 57 | logger({ 58 | console: { 59 | log: (...args) => console.log(...args), 60 | error: (...args) => console.error(...args), 61 | warn: (...args) => console.warn(...args), 62 | }, 63 | }), 64 | ], 65 | }); 66 | ``` 67 | 68 | ### `verbose` 69 | 70 | Enable or disable verbose mode. 71 | 72 | ```ts twoslash title="fetch.ts" 73 | import { createFetch } from "@better-fetch/fetch"; 74 | import { logger } from "@better-fetch/logger"; 75 | 76 | const $fetch = createFetch({ 77 | baseURL: "http://localhost:3000", 78 | plugins: [ 79 | logger({ 80 | verbose: true, 81 | }), 82 | ], 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /doc/lib/better-fetch-options.ts: -------------------------------------------------------------------------------- 1 | import { BetterFetchOption, BetterFetchPlugin } from "@better-fetch/fetch"; 2 | 3 | type BetterFetchOptions = Omit; 4 | export type { BetterFetchPlugin, BetterFetchOptions, BetterFetchOption }; 5 | -------------------------------------------------------------------------------- /doc/lib/example.ts: -------------------------------------------------------------------------------- 1 | import { createFetch } from "@better-fetch/fetch"; 2 | -------------------------------------------------------------------------------- /doc/lib/metadata.ts: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next/types"; 2 | 3 | export function createMetadata(override: Metadata): Metadata { 4 | return { 5 | ...override, 6 | openGraph: { 7 | title: override.title ?? undefined, 8 | description: override.description ?? undefined, 9 | url: "https://better-fetch.vercel.app", 10 | images: "https://better-fetch.vercel.app/banner.png", 11 | siteName: "Better Fetch", 12 | ...override.openGraph, 13 | }, 14 | twitter: { 15 | card: "summary_large_image", 16 | creator: "@beakcru", 17 | title: override.title ?? undefined, 18 | description: override.description ?? undefined, 19 | images: "https://better-fetch.vercel.app/banner.png", 20 | ...override.twitter, 21 | }, 22 | }; 23 | } 24 | 25 | export const baseUrl = 26 | process.env.NODE_ENV === "development" 27 | ? new URL("http://localhost:3000") 28 | : new URL(`https://${process.env.VERCEL_URL!}`); 29 | -------------------------------------------------------------------------------- /doc/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const baseUrl = 9 | process.env.NODE_ENV === "development" 10 | ? new URL("http://localhost:3000") 11 | : new URL(`https://${process.env.VERCEL_URL!}`); 12 | -------------------------------------------------------------------------------- /doc/mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import { AutoTypeTable } from "fumadocs-typescript/ui"; 2 | import { Callout } from "fumadocs-ui/components/callout"; 3 | import { Card, Cards } from "fumadocs-ui/components/card"; 4 | import { Tab, Tabs } from "fumadocs-ui/components/tabs"; 5 | import defaultComponents from "fumadocs-ui/mdx"; 6 | import { Popup, PopupContent, PopupTrigger } from "fumadocs-ui/twoslash/popup"; 7 | import type { MDXComponents } from "mdx/types"; 8 | import Link from "next/link"; 9 | 10 | export function useMDXComponents(components: MDXComponents): MDXComponents { 11 | return { 12 | ...defaultComponents, 13 | ...components, 14 | Popup, 15 | PopupContent, 16 | PopupTrigger, 17 | Tab, 18 | Tabs, 19 | Card, 20 | Cards, 21 | Callout, 22 | Link, 23 | AutoTypeTable, 24 | blockquote: (props) => {props.children}, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /doc/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 6 | -------------------------------------------------------------------------------- /doc/next.config.mjs: -------------------------------------------------------------------------------- 1 | import { rehypeCodeDefaultOptions } from "fumadocs-core/mdx-plugins"; 2 | import { remarkInstall } from "fumadocs-docgen"; 3 | import createMDX from "fumadocs-mdx/config"; 4 | import { transformerTwoslash } from "fumadocs-twoslash"; 5 | 6 | const withMDX = createMDX({ 7 | mdxOptions: { 8 | rehypeCodeOptions: { 9 | transformers: [ 10 | ...rehypeCodeDefaultOptions.transformers, 11 | transformerTwoslash(), 12 | ], 13 | }, 14 | remarkPlugins: [ 15 | [ 16 | remarkInstall, 17 | { 18 | persist: { 19 | id: "persist-install", 20 | }, 21 | }, 22 | ], 23 | ], 24 | }, 25 | }); 26 | 27 | /** @type {import('next').NextConfig} */ 28 | const config = { 29 | reactStrictMode: true, 30 | }; 31 | 32 | export default withMDX(config); 33 | -------------------------------------------------------------------------------- /doc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "doc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "start": "next start", 9 | "typecheck": "tsc --noEmit" 10 | }, 11 | "dependencies": { 12 | "@better-fetch/fetch": "^1.1.13", 13 | "@better-fetch/logger": "^1.1.0", 14 | "@radix-ui/react-icons": "^1.3.0", 15 | "@radix-ui/react-slot": "^1.1.0", 16 | "class-variance-authority": "^0.7.0", 17 | "clsx": "^2.1.1", 18 | "framer-motion": "^11.3.2", 19 | "fumadocs-core": "12.4.2", 20 | "fumadocs-docgen": "^1.1.0", 21 | "fumadocs-mdx": "8.2.33", 22 | "fumadocs-openapi": "^3.3.0", 23 | "fumadocs-twoslash": "^1.1.0", 24 | "fumadocs-typescript": "^2.0.1", 25 | "fumadocs-ui": "12.4.2", 26 | "next": "^14.2.4", 27 | "react": "^18.3.1", 28 | "react-dom": "^18.3.1", 29 | "tailwind-merge": "^2.5.2", 30 | "zod": "^3.24.0" 31 | }, 32 | "devDependencies": { 33 | "@types/mdx": "^2.0.13", 34 | "@types/node": "20.14.10", 35 | "@types/react": "^18.3.3", 36 | "@types/react-dom": "^18.3.0", 37 | "autoprefixer": "^10.4.19", 38 | "postcss": "^8.4.39", 39 | "tailwindcss": "^3.4.4", 40 | "typescript": "^5.5.3" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /doc/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /doc/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /doc/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /doc/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/apple-touch-icon.png -------------------------------------------------------------------------------- /doc/public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/banner.png -------------------------------------------------------------------------------- /doc/public/better-fetch-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /doc/public/better-fetch-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /doc/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/favicon-16x16.png -------------------------------------------------------------------------------- /doc/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/favicon-32x32.png -------------------------------------------------------------------------------- /doc/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch/a9168d52251f5e423ffb3e9e66b879fcfbf8d19a/doc/public/favicon.ico -------------------------------------------------------------------------------- /doc/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /doc/tailwind.config.js: -------------------------------------------------------------------------------- 1 | import { createPreset } from "fumadocs-ui/tailwind-plugin"; 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | export default { 5 | content: [ 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./content/**/*.{md,mdx}", 9 | "./mdx-components.{ts,tsx}", 10 | "./node_modules/fumadocs-ui/dist/**/*.js", 11 | ], 12 | presets: [createPreset()], 13 | theme: { 14 | extend: { 15 | colors: { 16 | border: "hsl(var(--border))", 17 | input: "hsl(var(--input))", 18 | ring: "hsl(var(--ring))", 19 | background: "hsl(var(--background))", 20 | foreground: "hsl(var(--foreground))", 21 | primary: { 22 | DEFAULT: "hsl(var(--primary))", 23 | foreground: "hsl(var(--primary-foreground))", 24 | }, 25 | secondary: { 26 | DEFAULT: "hsl(var(--secondary))", 27 | foreground: "hsl(var(--secondary-foreground))", 28 | }, 29 | destructive: { 30 | DEFAULT: "hsl(var(--destructive))", 31 | foreground: "hsl(var(--destructive-foreground))", 32 | }, 33 | muted: { 34 | DEFAULT: "hsl(var(--muted))", 35 | foreground: "hsl(var(--muted-foreground))", 36 | }, 37 | accent: { 38 | DEFAULT: "hsl(var(--accent))", 39 | foreground: "hsl(var(--accent-foreground))", 40 | }, 41 | popover: { 42 | DEFAULT: "hsl(var(--popover))", 43 | foreground: "hsl(var(--popover-foreground))", 44 | }, 45 | card: { 46 | DEFAULT: "hsl(var(--card))", 47 | foreground: "hsl(var(--card-foreground))", 48 | }, 49 | }, 50 | borderRadius: { 51 | lg: "var(--radius)", 52 | md: "calc(var(--radius) - 2px)", 53 | sm: "calc(var(--radius) - 4px)", 54 | }, 55 | animation: { 56 | ripple: "ripple var(--duration,2s) ease calc(var(--i, 0)*.2s) infinite", 57 | "accordion-down": "accordion-down 0.2s ease-out", 58 | "accordion-up": "accordion-up 0.2s ease-out", 59 | }, 60 | keyframes: { 61 | "accordion-down": { 62 | from: { height: "0" }, 63 | to: { height: "var(--radix-accordion-content-height)" }, 64 | }, 65 | "accordion-up": { 66 | from: { height: "var(--radix-accordion-content-height)" }, 67 | to: { height: "0" }, 68 | }, 69 | ripple: { 70 | "0%, 100%": { 71 | transform: "translate(-50%, -50%) scale(1)", 72 | }, 73 | "50%": { 74 | transform: "translate(-50%, -50%) scale(0.9)", 75 | }, 76 | }, 77 | }, 78 | }, 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /doc/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "ESNext", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "noEmit": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve", 17 | "incremental": true, 18 | "paths": { 19 | "@/*": ["./*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ] 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@better-fetch/root", 3 | "private": true, 4 | "version": "", 5 | "scripts": { 6 | "build": "pnpm --filter \"./packages/*\" build", 7 | "dev": "pnpm -F fetch dev", 8 | "test": "pnpm --filter \"./packages/*\" test", 9 | "bump": "bumpp \"./packages/*/package.json\"", 10 | "release": "pnpm --filter \"./packages/*\" build && bumpp && pnpm -r publish --access public", 11 | "typecheck": "pnpm -r typecheck", 12 | "lint": "biome check .", 13 | "format": "biome check . --apply" 14 | }, 15 | "dependencies": { 16 | "@biomejs/biome": "1.7.3", 17 | "simple-git-hooks": "^2.11.1", 18 | "tinyglobby": "^0.2.9", 19 | "vitest": "^1.5.0" 20 | }, 21 | "simple-git-hooks": { 22 | "pre-push": "pnpm typecheck" 23 | }, 24 | "devDependencies": { 25 | "bumpp": "^9.4.1", 26 | "tsup": "^8.0.2" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/better-fetch/.npmrc: -------------------------------------------------------------------------------- 1 | //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN} -------------------------------------------------------------------------------- /packages/better-fetch/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v0.0.1 4 | 5 | ### 🚀 First Release 6 | 7 | - Works 8 | 9 | ## v0.0.2 10 | 11 | ### 🚀 Release 12 | 13 | - Fetch schema now has a runtime check with zod 14 | - Strict helper function to create a fetch schema with strict mode 15 | - Dynamic parameters can be defined with Fetch Schema. 16 | - Using method modifiers you can define the request method on the fetch schema. -------------------------------------------------------------------------------- /packages/better-fetch/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Bereket Engida 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/better-fetch/README.md: -------------------------------------------------------------------------------- 1 | # Better Fetch 2 | 3 | Advanced fetch wrapper for typescript with zod schema validations, pre-defined routes, hooks, plugins and more. Works on the browser, node (version 18+), workers, deno and bun. 4 | 5 | # Documentation 6 | 7 | https://better-fetch.vercel.app/ 8 | 9 | ## License 10 | 11 | MIT -------------------------------------------------------------------------------- /packages/better-fetch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@better-fetch/fetch", 3 | "version": "1.1.18", 4 | "main": "./dist/index.cjs", 5 | "type": "module", 6 | "module": "./dist/index.js", 7 | "react-native": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "scripts": { 10 | "build": "tsup", 11 | "dev": "tsup --watch", 12 | "test": "vitest", 13 | "bump": "bumpp", 14 | "test:watch": "vitest watch", 15 | "lint": "biome check .", 16 | "release": "bun run build && npm publish --access public", 17 | "lint:fix": "biome check . --apply", 18 | "typecheck": "tsc --noEmit" 19 | }, 20 | "devDependencies": { 21 | "@happy-dom/global-registrator": "^14.7.1", 22 | "@testing-library/dom": "^10.0.0", 23 | "@testing-library/react": "^15.0.3", 24 | "@types/bun": "^1.1.0", 25 | "@types/node": "^20.11.30", 26 | "bumpp": "^9.4.1", 27 | "global-jsdom": "^24.0.0", 28 | "h3": "^1.11.1", 29 | "jsdom": "^24.0.0", 30 | "listhen": "^1.7.2", 31 | "mocha": "^10.4.0", 32 | "tsup": "^8.0.2", 33 | "typescript": "^5.4.5", 34 | "vitest": "^1.5.0", 35 | "zod": "^3.24.1" 36 | }, 37 | "exports": { 38 | ".": { 39 | "import": "./dist/index.js", 40 | "require": "./dist/index.cjs" 41 | } 42 | }, 43 | "files": ["dist"] 44 | } 45 | -------------------------------------------------------------------------------- /packages/better-fetch/src/auth.ts: -------------------------------------------------------------------------------- 1 | import type { BetterFetchOption } from "./types"; 2 | 3 | export type typeOrTypeReturning = T | (() => T); 4 | /** 5 | * Bearer token authentication 6 | * 7 | * the value of `token` will be added to a header as 8 | * `auth: Bearer token`, 9 | */ 10 | export type Bearer = { 11 | type: "Bearer"; 12 | token: typeOrTypeReturning>; 13 | }; 14 | 15 | /** 16 | * Basic auth 17 | */ 18 | export type Basic = { 19 | type: "Basic"; 20 | username: typeOrTypeReturning; 21 | password: typeOrTypeReturning; 22 | }; 23 | 24 | /** 25 | * Custom auth 26 | * 27 | * @param prefix - prefix of the header 28 | * @param value - value of the header 29 | * 30 | * @example 31 | * ```ts 32 | * { 33 | * type: "Custom", 34 | * prefix: "Token", 35 | * value: "token" 36 | * } 37 | * ``` 38 | */ 39 | export type Custom = { 40 | type: "Custom"; 41 | prefix: typeOrTypeReturning; 42 | value: typeOrTypeReturning; 43 | }; 44 | 45 | export type Auth = Bearer | Basic | Custom; 46 | 47 | export const getAuthHeader = async (options?: BetterFetchOption) => { 48 | const headers: Record = {}; 49 | const getValue = async ( 50 | value: typeOrTypeReturning< 51 | string | undefined | Promise 52 | >, 53 | ) => (typeof value === "function" ? await value() : value); 54 | if (options?.auth) { 55 | if (options.auth.type === "Bearer") { 56 | const token = await getValue(options.auth.token); 57 | if (!token) { 58 | return headers; 59 | } 60 | headers["authorization"] = `Bearer ${token}`; 61 | } else if (options.auth.type === "Basic") { 62 | const username = getValue(options.auth.username); 63 | const password = getValue(options.auth.password); 64 | if (!username || !password) { 65 | return headers; 66 | } 67 | headers["authorization"] = `Basic ${btoa(`${username}:${password}`)}`; 68 | } else if (options.auth.type === "Custom") { 69 | const value = getValue(options.auth.value); 70 | if (!value) { 71 | return headers; 72 | } 73 | headers["authorization"] = `${getValue(options.auth.prefix)} ${value}`; 74 | } 75 | } 76 | return headers; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/better-fetch/src/create-fetch/index.ts: -------------------------------------------------------------------------------- 1 | import { betterFetch } from "../fetch"; 2 | import { BetterFetchPlugin } from "../plugins"; 3 | import type { BetterFetchOption } from "../types"; 4 | import { parseStandardSchema } from "../utils"; 5 | import type { BetterFetch, CreateFetchOption } from "./types"; 6 | 7 | export const applySchemaPlugin = (config: CreateFetchOption) => 8 | ({ 9 | id: "apply-schema", 10 | name: "Apply Schema", 11 | version: "1.0.0", 12 | async init(url, options) { 13 | const schema = 14 | config.plugins?.find((plugin) => 15 | plugin.schema?.config 16 | ? url.startsWith(plugin.schema.config.baseURL || "") || 17 | url.startsWith(plugin.schema.config.prefix || "") 18 | : false, 19 | )?.schema || config.schema; 20 | if (schema) { 21 | let urlKey = url; 22 | if (schema.config?.prefix) { 23 | if (urlKey.startsWith(schema.config.prefix)) { 24 | urlKey = urlKey.replace(schema.config.prefix, ""); 25 | if (schema.config.baseURL) { 26 | url = url.replace(schema.config.prefix, schema.config.baseURL); 27 | } 28 | } 29 | } 30 | if (schema.config?.baseURL) { 31 | if (urlKey.startsWith(schema.config.baseURL)) { 32 | urlKey = urlKey.replace(schema.config.baseURL, ""); 33 | } 34 | } 35 | const keySchema = schema.schema[urlKey]; 36 | if (keySchema) { 37 | let opts = { 38 | ...options, 39 | method: keySchema.method, 40 | output: keySchema.output, 41 | }; 42 | if (!options?.disableValidation) { 43 | opts = { 44 | ...opts, 45 | body: keySchema.input 46 | ? await parseStandardSchema(keySchema.input, options?.body) 47 | : options?.body, 48 | params: keySchema.params 49 | ? await parseStandardSchema(keySchema.params, options?.params) 50 | : options?.params, 51 | query: keySchema.query 52 | ? await parseStandardSchema(keySchema.query, options?.query) 53 | : options?.query, 54 | }; 55 | } 56 | return { 57 | url, 58 | options: opts, 59 | }; 60 | } 61 | } 62 | return { 63 | url, 64 | options, 65 | }; 66 | }, 67 | }) satisfies BetterFetchPlugin; 68 | 69 | export const createFetch =