├── .gitignore ├── README.md ├── app ├── api │ ├── 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 ├── 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 │ ├── 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 ├── extra-content.md ├── lib ├── better-fetch-options.ts ├── example.ts ├── metadata.ts └── utils.ts ├── mdx-components.tsx ├── next.config.mjs ├── package.json ├── pnpm-lock.yaml ├── 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 /.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 -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /app/api/og/inter-bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/app/api/og/inter-bold.woff -------------------------------------------------------------------------------- /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 | 12 | export const runtime = 'edge'; 13 | 14 | const bold = fetch(new URL('./inter-bold.woff', import.meta.url)).then((res) => 15 | res.arrayBuffer(), 16 | ); 17 | 18 | const foreground = 'hsl(0 0% 98%)'; 19 | const mutedForeground = 'hsl(0 0% 63.9%)'; 20 | const background = 'rgba(10, 10, 10)'; 21 | 22 | export async function GET( 23 | request: NextRequest, 24 | ): Promise { 25 | const { searchParams } = request.nextUrl; 26 | const title = searchParams.get('title'), 27 | description = searchParams.get('description'); 28 | 29 | return new ImageResponse( 30 | OG({ 31 | title: title ?? 'Better Fetch', 32 | description: description ?? 'Advanced fetch library for typescript.', 33 | }), 34 | { 35 | width: 1200, 36 | height: 630, 37 | fonts: [{ name: 'Inter', data: await bold, weight: 700 }], 38 | }, 39 | ); 40 | } 41 | 42 | function OG({ 43 | title, 44 | description, 45 | }: { 46 | title: string; 47 | description: string; 48 | }): React.ReactElement { 49 | return ( 50 |
57 |
64 |
71 |

{title}

72 |

78 | {description} 79 |

80 |
81 |
82 | 83 |
84 | 94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 | ); 103 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getPage, getPages } from '@/app/source'; 2 | import type { Metadata } from 'next'; 3 | import { DocsPage, DocsBody } from 'fumadocs-ui/page'; 4 | import { notFound } from 'next/navigation'; 5 | import { Card, Cards } from 'fumadocs-ui/components/card'; 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 | 23 | 24 |

{page.data.title}

25 | 29 |
30 |
31 | ); 32 | } 33 | 34 | export async function generateStaticParams() { 35 | return getPages().map((page) => ({ 36 | slug: page.slugs, 37 | })); 38 | } 39 | 40 | export function generateMetadata({ params }: { params: { slug?: string[] } }) { 41 | const page = getPage(params.slug); 42 | 43 | if (page == null) notFound(); 44 | 45 | return { 46 | title: page.data.title, 47 | description: page.data.description, 48 | } satisfies Metadata; 49 | } 50 | -------------------------------------------------------------------------------- /app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from 'fumadocs-ui/layout'; 2 | import type { ReactNode } from 'react'; 3 | import { docsOptions } from '../layout.config'; 4 | import { GitHubLogoIcon } from '@radix-ui/react-icons'; 5 | import Link from 'next/link'; 6 | 7 | export default function Layout({ children }: { children: ReactNode }) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/layout.config.tsx: -------------------------------------------------------------------------------- 1 | import { type BaseLayoutProps, type DocsLayoutProps } from 'fumadocs-ui/layout'; 2 | import { pageTree } from '@/app/source'; 3 | import Link from 'next/link'; 4 | import { GitHubLogoIcon } from '@radix-ui/react-icons'; 5 | 6 | // shared configuration 7 | export const baseOptions: BaseLayoutProps = { 8 | nav: { 9 | title: "Better-Fetch", 10 | transparentMode: "top", 11 | }, 12 | links: [], 13 | 14 | }; 15 | 16 | // docs layout configuration 17 | export const docsOptions: DocsLayoutProps = { 18 | ...baseOptions, 19 | tree: pageTree, 20 | nav: { 21 | title: "Better Fetch", 22 | }, 23 | sidebar: { 24 | collapsible: false, 25 | footer: ( 26 | 27 | 28 | 29 | ), 30 | defaultOpenLevel: 1, 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /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 { Viewport } from 'next'; 7 | import { baseUrl, createMetadata } from '@/lib/metadata'; 8 | 9 | const inter = Inter({ 10 | subsets: ['latin'], 11 | }); 12 | 13 | 14 | export const metadata = createMetadata({ 15 | title: { 16 | template: '%s | Better Fetch', 17 | default: 'Better Fetch', 18 | }, 19 | description: 'Advanced fetch wrapper for typescript.', 20 | metadataBase: baseUrl, 21 | }); 22 | 23 | export default function Layout({ children }: { children: ReactNode }) { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { Button } from "@/components/ui/button" 3 | import { Icons } from "@/components/icons" 4 | import { cn } from '@/lib/utils'; 5 | import AnimatedGridPattern from "@/components/landing/grid"; 6 | import Ripple from '@/components/landing/ripple'; 7 | import { Banner } from 'fumadocs-ui/components/banner'; 8 | 9 | 10 | 11 | export default function HomePage() { 12 | return ( 13 |
14 | 15 | 16 |
17 |

18 | Better Fetch 19 |

20 | 21 |

22 | Advanced fetch wrapper for typescript with zod schema validations, pre-defined routes, callbacks, plugins and more. 23 |

24 |
25 | 26 | 30 | 31 | 32 | 36 | 37 |
38 |
39 |
40 |
41 | 42 |
43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/source.ts: -------------------------------------------------------------------------------- 1 | import { map } from '@/.map'; 2 | import { createMDXSource } from 'fumadocs-mdx'; 3 | import { loader } from 'fumadocs-core/source'; 4 | 5 | export const { getPage, getPages, pageTree } = loader({ 6 | baseUrl: '/docs', 7 | rootDir: 'docs', 8 | source: createMDXSource(map), 9 | }); 10 | -------------------------------------------------------------------------------- /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 | 16 | ), 17 | docs: () => ( 18 | 19 | ), 20 | trafficLights: TrafficLightsIcon 21 | } -------------------------------------------------------------------------------- /components/landing/animated-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { motion, type AnimationProps } 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 | -------------------------------------------------------------------------------- /components/landing/box-reveal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useRef } from "react"; 4 | import { motion, useAnimation, useInView } from "framer-motion"; 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 | -------------------------------------------------------------------------------- /components/landing/grid.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect, useId, useRef, useState } from "react"; 4 | import { motion } from "framer-motion"; 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 | -------------------------------------------------------------------------------- /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.20, 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 |
40 | ); 41 | })} 42 |
43 | ); 44 | }); 45 | 46 | Ripple.displayName = "Ripple"; 47 | 48 | export default Ripple; 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 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 | -------------------------------------------------------------------------------- /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 promise that resolves to 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 | 36 | const $fetch = createFetch({ 37 | baseURL: "http://localhost:3000", 38 | auth: { 39 | type: "Bearer", 40 | token: () => authStore.getToken(), 41 | }, 42 | }) 43 | ``` 44 | 45 | 46 | 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. 47 | 48 | 49 | ### Basic 50 | 51 | The basic authorization is used to add a basic authentication to the request. The username and password are added to the `Authorization` header. 52 | 53 | ```ts twoslash title="fetch.ts" 54 | import { createFetch } from "@better-fetch/fetch"; 55 | 56 | const $fetch = createFetch({ 57 | baseURL: "http://localhost:3000", 58 | auth: { 59 | type: "Basic", 60 | username: "my-username", 61 | password: "my-password", 62 | }, 63 | }) 64 | ``` -------------------------------------------------------------------------------- /content/docs/default-types.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Deafult Types 3 | description: Deafult Types 4 | --- 5 | 6 | 7 | ## Default Output 8 | 9 | By default the response data will always be type of `unknown`. If you want to customize the deafult type you can pass `defaultOutput` option to the `createFetch` function. 10 | 11 | 12 | This only serve as a type for the response data it's not used as a validation schema. 13 | 14 | 15 | ```ts twoslash title="fetch.ts" 16 | import { createFetch } from "@better-fetch/fetch"; 17 | import { z } from "zod"; 18 | 19 | const $fetch = createFetch({ 20 | baseURL: "https://jsonplaceholder.typicode.com", 21 | defaultOutput: z.any(), 22 | }) 23 | 24 | const { data, error } = await $fetch("/todos/1") 25 | 26 | // @annotate: Hover over the data object to see the type 27 | ``` 28 | 29 | If you define output schema, the default output type will be ignored. 30 | 31 | ```ts twoslash title="fetch.ts" 32 | import { createFetch } from "@better-fetch/fetch"; 33 | import { z } from "zod"; 34 | 35 | const $fetch = createFetch({ 36 | baseURL: "https://jsonplaceholder.typicode.com", 37 | defaultOutput: z.any(), 38 | }); 39 | 40 | const { data, error } = await $fetch("/todos/1", { 41 | output: z.object({ 42 | userId: z.string(), 43 | id: z.number(), 44 | title: z.string(), 45 | completed: z.boolean(), 46 | }), 47 | }) 48 | // @annotate: Hover over the data object to see the type 49 | ``` 50 | 51 | ## Default error 52 | 53 | The default error type is: 54 | ```ts 55 | { status: number, statusText: string, message?: string }. 56 | ``` 57 | 58 | if you want custom defualt error type, you can pass a `defautlError` option to the `createFetch` function. 59 | 60 | 61 | The `status` and `statusText` properties are always defined. Your custom error definations are only 62 | infered if the api returns a json error object. 63 | 64 | 65 | ```ts twoslash title="fetch.ts" 66 | import { createFetch } from "@better-fetch/fetch"; 67 | import { z } from "zod"; 68 | 69 | const $fetch = createFetch({ 70 | baseURL: "https://jsonplaceholder.typicode.com", 71 | defaultError: z.object({ 72 | message: z.string().optional(), 73 | error: z.string(), 74 | }), 75 | }) 76 | 77 | const { data, error } = await $fetch("/todos/1") 78 | // @annotate: Hover over the error object to see the type 79 | ``` 80 | 81 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 reqeust and response data. This is like **trpc** or other typed rpc librires client but you define it yourself. 7 | 8 | The output of the scheam will be validated using zod and if the validation fails, it'll throw an error. 9 | 10 | Before we start, make sure you have installed the [zod](https://www.npmjs.com/package/zod) package. 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 | 23 | export const schema = createSchema({ // [!code highlight] 24 | "/path": { // [!code highlight] 25 | input: z.object({ // [!code highlight] 26 | userId: z.string(), // [!code highlight] 27 | id: z.number(), // [!code highlight] 28 | title: z.string(), // [!code highlight] 29 | completed: z.boolean(), // [!code highlight] 30 | }), // [!code highlight] 31 | output: z.object({ // [!code highlight] 32 | userId: z.string(), // [!code highlight] 33 | id: z.number(), // [!code highlight] 34 | title: z.string(), // [!code highlight] 35 | completed: z.boolean(), // [!code highlight] 36 | }), // [!code highlight] 37 | } // [!code highlight] 38 | }) // [!code highlight] 39 | 40 | const $fetch = createFetch({ 41 | baseUrl: "https://jsonplaceholder.typicode.com", 42 | schema: schema // [!code highlight] 43 | }); 44 | 45 | ``` 46 | 47 | 48 | ## Fetch Schema 49 | 50 | The Fetch Schema is a map of path/url and schema. The path is the url path and the schema is an object with 51 | `input`, `output`, `query` and `params` keys. 52 | 53 | 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. 54 | 55 | ### Input 56 | 57 | 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 requeired to be passed as a body of the request. 58 | 59 | 60 | 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 specefic methods. 61 | 62 | 63 | ```ts twoslash title="fetch.ts" 64 | import { createFetch, createSchema } from "@better-fetch/fetch"; 65 | import { z } from "zod"; 66 | 67 | const $fetch = createFetch({ 68 | baseUrl: "https://jsonplaceholder.typicode.com", 69 | schema: createSchema({ 70 | "/path": { 71 | input: z.object({ 72 | userId: z.string(), 73 | id: z.number(), 74 | title: z.string(), 75 | completed: z.boolean(), 76 | }), 77 | }, 78 | }), 79 | }) 80 | 81 | // @errors: 2739 82 | const { data, error } = await $fetch("/path", { 83 | body: {} 84 | }) 85 | ``` 86 | 87 | To make the body optional you can wrap the schema with `z.optional`. 88 | 89 | 90 | ### Output 91 | 92 | The output schema is the schema of the response data. 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. 93 | 94 | 95 | ```ts twoslash title="fetch.ts" 96 | import { createFetch, createSchema } from "@better-fetch/fetch"; 97 | import { z } from "zod"; 98 | 99 | const $fetch = createFetch({ 100 | baseUrl: "https://jsonplaceholder.typicode.com", 101 | schema: createSchema({ 102 | "/path": { 103 | output: z.object({ 104 | userId: z.string(), 105 | id: z.number(), 106 | title: z.string(), 107 | completed: z.boolean(), 108 | }), 109 | }, 110 | }), 111 | }) 112 | 113 | const { data, error } = await $fetch("/path") 114 | // @annotate: Hover over the data object to see the type 115 | ``` 116 | 117 | 118 | ### Query 119 | 120 | 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. 121 | 122 | ```ts twoslash title="fetch.ts" 123 | import { createFetch, createSchema } from "@better-fetch/fetch"; 124 | import { z } from "zod"; 125 | 126 | const $fetch = createFetch({ 127 | baseUrl: "https://jsonplaceholder.typicode.com", 128 | schema: createSchema({ 129 | "/path": { 130 | query: z.object({ 131 | userId: z.string(), 132 | id: z.number(), 133 | title: z.string(), 134 | completed: z.boolean(), 135 | }), 136 | }, 137 | }), 138 | }) 139 | 140 | // @errors: 2739 141 | const { data, error } = await $fetch("/path", { 142 | query: {} 143 | }) 144 | // @annotate: Hover over the data object to see the type 145 | ``` 146 | 147 | ### Dynamic Path Parameters 148 | 149 | 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. 150 | 151 | 152 | 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. 153 | 154 | 155 | ```ts twoslash title="fetch.ts" 156 | import { createFetch, createSchema } from "@better-fetch/fetch"; 157 | import { z } from "zod"; 158 | 159 | const schema = createSchema({ 160 | "/user/:id": { 161 | output: z.object({ 162 | name: z.string(), 163 | }), 164 | }, 165 | "/post": { 166 | params: z.object({ 167 | id: z.string(), 168 | title: z.string(), 169 | }), 170 | }, 171 | "/post/:id/:title": { 172 | output: z.object({ 173 | title: z.string(), 174 | }), 175 | } 176 | }) 177 | 178 | 179 | const $fetch = createFetch({ 180 | baseUrl: "https://jsonplaceholder.typicode.com", 181 | schema: schema 182 | }) 183 | 184 | const response1 = await $fetch("/user/:id", { 185 | params: { 186 | id: "1", 187 | } 188 | }) 189 | 190 | const response2 = await $fetch("/post", { 191 | params: { 192 | id: "1", 193 | title: "title" 194 | }, 195 | }) 196 | 197 | const response3 = await $fetch("/post/:id/:title", { 198 | params: [ 199 | "1", 200 | "title" 201 | ] 202 | }) 203 | 204 | ``` 205 | 206 | 207 | ### Method Modifiers 208 | 209 | By default the `get` and `post` methods are used to make the request based on weather the input schema is defined or not. You can use the `method` modifier to define the method to be used. 210 | 211 | The method modifiers are `@get`, `@post`, `@put`, `@patch`, `@delete` and `@head`. You prepend the method name to the path to define the method 212 | 213 | ```ts twoslash title="fetch.ts" 214 | import { createFetch, createSchema } from "@better-fetch/fetch"; 215 | import { z } from "zod"; 216 | 217 | const $fetch = createFetch({ 218 | baseUrl: "https://jsonplaceholder.typicode.com", 219 | schema: createSchema({ 220 | "/@put/user": { // [!code highlight] 221 | input: z.object({ 222 | title: z.string(), 223 | completed: z.boolean(), 224 | }), 225 | output: z.object({ 226 | title: z.string(), 227 | completed: z.boolean(), 228 | }), 229 | }, 230 | }), 231 | }) 232 | 233 | const { data, error } = await $fetch("/@put/user", { 234 | body: { 235 | title: "title", 236 | completed: true, 237 | } 238 | }) 239 | // @annotate: the request will be made to "/user" path with a PUT method. 240 | ``` 241 | 242 | 243 | ## Strict Schema 244 | 245 | 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. 246 | 247 | ```ts twoslash title="fetch.ts" 248 | import { createFetch, createSchema } from "@better-fetch/fetch"; 249 | import { z } from "zod"; 250 | 251 | const $fetch = createFetch({ 252 | baseUrl: "https://jsonplaceholder.typicode.com", 253 | schema: createSchema({ 254 | "/path": { 255 | output: z.object({ 256 | userId: z.string(), 257 | id: z.number(), 258 | title: z.string(), 259 | completed: z.boolean(), 260 | }), 261 | }, 262 | }, 263 | { // [!code highlight] 264 | strict: true // [!code highlight] 265 | }), // [!code highlight] 266 | }) 267 | // @errors: 2345 268 | const { data, error } = await $fetch("/invalid-path") 269 | ``` 270 | 271 | 272 | -------------------------------------------------------------------------------- /content/docs/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | description: Getting started with Better Fetch 4 | --- 5 | 6 | ### Installation 7 | 8 | 9 | ```package-install 10 | npm i @better-fetch/fetch 11 | ``` 12 | 13 | *Optionally you can also install the [zod](https://www.npmjs.com/package/zod) package to use zod schema validations.* 14 | 15 | ```package-install 16 | npm i zod 17 | ``` 18 | 19 | ### Quick Start 20 | 21 | The fastest way to start using better fetch is to import the `betterFetch` function and start making requests. 22 | 23 | You can define the response type using generics or **zod schema (recommended)**. 24 | 25 | 1. Using generics 26 | 27 | ```ts twoslash title="with-generics.ts" 28 | import { betterFetch } from '@better-fetch/fetch'; 29 | 30 | //Using generic type 31 | const { data, error } = await betterFetch<{ 32 | userId: string; 33 | id: number; 34 | title: string; 35 | completed: boolean; 36 | }>("https://jsonplaceholder.typicode.com/todos/1"); 37 | ``` 38 | 39 | 2. Using zod schema 40 | 41 | ```ts twoslash title="with-zod.ts" 42 | import { betterFetch } from '@better-fetch/fetch'; 43 | import { z } from 'zod'; 44 | 45 | //Using zod schema 46 | const { data: todos, error: todoError } = await betterFetch("https://jsonplaceholder.typicodei.com/todos/1", { 47 | output: z.object({ 48 | userId: z.string(), 49 | id: z.number(), 50 | title: z.string(), 51 | completed: z.boolean(), 52 | }) 53 | }); 54 | ``` 55 | 56 | Better fetch by default returns a `Promise` that resolves to an object of `data` and `error`. Hover over the example code block above to see the type of the response. 57 | 58 | 59 | **data** by defualt is of type `unknown` or `null` incase of a response error. 60 | 61 | 62 | You can define the type of the response data in 2 ways: 63 | 64 | 1. **Using the `output` property to define a zod schema (recommended)**: The `output` property is used to define the type of the response data. The response data will be validated against the schema. If the response data does not match the schema, an error will be thrown. 65 | 66 | 2. **By passing a generic type to the `betterFetch`**: You can also pass a generic type to the `betterFetch` function. It has some drawbacks but it should work in most cases. 67 | 68 | 69 | Make sure strict mode is enabled in your tsconfig when using zod schema validations. 70 | 71 | 72 | ### Create Fetch 73 | 74 | Create Fetch allows you to create a better fetch instance with custom configurations. 75 | 76 | ```ts twoslash title="fetch.ts" 77 | import { createFetch } from "@better-fetch/fetch"; 78 | 79 | export const $fetch = createFetch({ 80 | baseUrl: "https://jsonplaceholder.typicode.com", 81 | retry: { 82 | count: 2, 83 | }, 84 | }); 85 | 86 | const { data, error } = await $fetch<{ 87 | userId: number; 88 | id: number; 89 | title: string; 90 | completed: boolean; 91 | }>("/todos/1"); 92 | ``` 93 | You can pass more options see the [Fetch Options](/docs/fetch-options) section for more details. 94 | 95 | ### Throwing Errors 96 | 97 | You can throw errors instead of returning them by passing the `throw` option. 98 | 99 | 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. 100 | 101 | ```ts twoslash title="fetch.ts" 102 | import { createFetch } from '@better-fetch/fetch'; 103 | import { z } from 'zod'; 104 | 105 | const $fetch = createFetch({ 106 | baseUrl: "https://jsonplaceholder.typicode.com", 107 | throw: true, 108 | }); 109 | 110 | const data = await $fetch<{ 111 | userId: number; 112 | }>("https://jsonplaceholder.typicode.com/todos/1"); 113 | ``` 114 | Learn more about handling errors [Handling Errors](/docs/handling-errors) section. 115 | 116 | -------------------------------------------------------------------------------- /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: { 76 | count: 2, 77 | }, 78 | throw: true, 79 | }); 80 | 81 | 82 | const data = await $fetch<{ 83 | userId: number; 84 | id: number; 85 | title: string; 86 | completed: boolean; 87 | }>("/todos/1"); 88 | ``` 89 | 90 | 2. Pass false as a second generic argument to the `betterFetch` function. 91 | 92 | ```ts twoslash title="fetch.ts" 93 | import { betterFetch } from '@better-fetch/fetch'; 94 | import { z } from 'zod'; 95 | 96 | const data = await betterFetch<{ 97 | userId: number; 98 | id: number; 99 | title: string; 100 | completed: boolean; 101 | }, 102 | false // [!code highlight] 103 | >("https://jsonplaceholder.typicode.com/todos/1"); 104 | ``` -------------------------------------------------------------------------------- /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 | ``` -------------------------------------------------------------------------------- /content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Getting started with Better Fetch 4 | --- 5 | 6 | Better fetch is advanced fetch wrapper for typescript. It supports error as value, zod schema validations, advanced type inference, pre-defined routes, hooks, plugins and more. Works on the browser, node (version 18+), workers, deno and bun. 7 | 8 | ## Features 9 | 10 | 14 | 18 | 22 | 26 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /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 | "timeout-and-retry", 12 | "---Advanced---", 13 | "fetch-schema", 14 | "default-types", 15 | "hooks", 16 | "plugins", 17 | "---Reference---", 18 | "fetch-options", 19 | "extra-options" 20 | ] 21 | } -------------------------------------------------------------------------------- /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. 7 | 8 | The plugins api allows you to modify the request, response, error and other parts of the request lifecycle. 9 | 10 | 11 | ### Init 12 | 13 | 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`. 14 | 15 | ```ts twoslash title="fetch.ts" 16 | import { createFetch, BetterFetchPlugin } from "@better-fetch/fetch"; 17 | 18 | const myPlugin = { 19 | id: "my-plugin", 20 | name: "My Plugin", 21 | init: async (url, options) => { 22 | if(url.startsWith("http://")) { 23 | const _url = new URL(url) 24 | const DEV_URL = "http://localhost:3000" 25 | return { 26 | url: `${DEV_URL}/${_url.pathname}`, 27 | options, 28 | } 29 | } 30 | return { 31 | url, 32 | options, 33 | } 34 | }, 35 | } satisfies BetterFetchPlugin; 36 | 37 | const $fetch = createFetch({ 38 | baseUrl: "https://jsonplaceholder.typicode.com", 39 | plugins: [myPlugin], 40 | }); 41 | ``` 42 | 43 | ### Hooks 44 | 45 | Hooks are functions that are called at different stages of the request lifecycle. See [Hooks](/docs/hooks) for more information. 46 | 47 | 48 | ```ts twoslash title="fetch.ts" 49 | import { createFetch, BetterFetchPlugin } from "@better-fetch/fetch"; 50 | 51 | const myPlugin = { 52 | id: "my-plugin", 53 | name: "My Plugin", 54 | hooks: { 55 | onRequest(context) { 56 | // do something with the context 57 | return context; 58 | }, 59 | onResponse(context) { 60 | // do something with the context 61 | return context.response; 62 | }, 63 | onError(context) { 64 | // do something with the context 65 | }, 66 | onSuccess(context) { 67 | // do something with the context 68 | }, 69 | } 70 | } satisfies BetterFetchPlugin; 71 | 72 | const $fetch = createFetch({ 73 | baseUrl: "https://jsonplaceholder.typicode.com", 74 | plugins: [myPlugin], 75 | }); 76 | ``` 77 | 78 | 79 | If more than one plugin is registered, the hooks will be called in the order they are registered. 80 | 81 | 82 | 83 | ### Properties 84 | 85 | -------------------------------------------------------------------------------- /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: { 41 | count: 3, 42 | interval: 1000, //optional 43 | } 44 | }); 45 | ``` -------------------------------------------------------------------------------- /extra-content.md: -------------------------------------------------------------------------------- 1 | ### DisableValidation 2 | 3 | If you want to bypass the validation, you can pass `disableValidation` as true. 4 | 5 | ```ts 6 | import { betterFetch } from 'better-fetch'; 7 | import { z } from 'zod'; 8 | 9 | const { data, error } = await betterFetch("https://jsonplaceholder.typicode.com/todos/1", { 10 | output: z.object({ 11 | userId: z.string(), 12 | id: z.number(), 13 | title: z.string(), 14 | completed: z.boolean(), 15 | }), 16 | disableValidation: true 17 | }); 18 | ``` -------------------------------------------------------------------------------- /lib/better-fetch-options.ts: -------------------------------------------------------------------------------- 1 | import { BetterFetchOption, BetterFetchPlugin } from "@better-fetch/fetch" 2 | 3 | 4 | type BetterFetchOptions = Omit 5 | export type { BetterFetchPlugin, BetterFetchOptions, BetterFetchOption } -------------------------------------------------------------------------------- /lib/example.ts: -------------------------------------------------------------------------------- 1 | import { createFetch } from "@better-fetch/fetch"; 2 | 3 | -------------------------------------------------------------------------------- /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!}`); -------------------------------------------------------------------------------- /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!}`); -------------------------------------------------------------------------------- /mdx-components.tsx: -------------------------------------------------------------------------------- 1 | import type { MDXComponents } from 'mdx/types'; 2 | import defaultComponents from 'fumadocs-ui/mdx'; 3 | import { Popup, PopupContent, PopupTrigger } from 'fumadocs-ui/twoslash/popup'; 4 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; 5 | import { Callout } from 'fumadocs-ui/components/callout'; 6 | import Link from 'next/link'; 7 | import { AutoTypeTable } from 'fumadocs-typescript/ui'; 8 | import { Card, Cards } from 'fumadocs-ui/components/card'; 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 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | import createMDX from 'fumadocs-mdx/config'; 2 | import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; 3 | import { transformerTwoslash } from 'fumadocs-twoslash'; 4 | import { remarkInstall } from 'fumadocs-docgen'; 5 | const withMDX = createMDX({ 6 | mdxOptions: { 7 | rehypeCodeOptions: { 8 | transformers: [ 9 | ...rehypeCodeDefaultOptions.transformers, 10 | transformerTwoslash(), 11 | ], 12 | }, 13 | remarkPlugins: [ 14 | [ 15 | remarkInstall, { 16 | persist: { 17 | id: 'persist-install', 18 | }, 19 | },] 20 | ], 21 | }, 22 | }); 23 | 24 | /** @type {import('next').NextConfig} */ 25 | const config = { 26 | reactStrictMode: true, 27 | }; 28 | 29 | export default withMDX(config); 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-fetch-doc", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "dev": "next dev", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@better-fetch/fetch": "1.0.1-beta.7", 12 | "@radix-ui/react-icons": "^1.3.0", 13 | "@radix-ui/react-slot": "^1.1.0", 14 | "class-variance-authority": "^0.7.0", 15 | "clsx": "^2.1.1", 16 | "framer-motion": "^11.3.2", 17 | "fumadocs-core": "12.4.2", 18 | "fumadocs-docgen": "^1.1.0", 19 | "fumadocs-mdx": "8.2.33", 20 | "fumadocs-openapi": "^3.3.0", 21 | "fumadocs-twoslash": "^1.1.0", 22 | "fumadocs-typescript": "^2.0.1", 23 | "fumadocs-ui": "12.4.2", 24 | "next": "^14.2.4", 25 | "react": "^18.3.1", 26 | "react-dom": "^18.3.1", 27 | "tailwind-merge": "^2.4.0", 28 | "zod": "^3.23.8" 29 | }, 30 | "devDependencies": { 31 | "@types/mdx": "^2.0.13", 32 | "@types/node": "20.14.10", 33 | "@types/react": "^18.3.3", 34 | "@types/react-dom": "^18.3.0", 35 | "autoprefixer": "^10.4.19", 36 | "postcss": "^8.4.39", 37 | "tailwindcss": "^3.4.4", 38 | "typescript": "^5.5.3" 39 | } 40 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/banner.png -------------------------------------------------------------------------------- /public/better-fetch-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/better-fetch-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bekacru/better-fetch-docs/ca639bbc10612d5a90a78dce783b19b77ebd2071/public/favicon.ico -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------