The native Web fetch() API is extended with automatic request deduplication and caching. Use it in Server Components and Route Handlers to fetch data efficiently with built-in performance optimizations powered by Rust.

Import

// fetch is globally available - no import needed
const data = await fetch('https://api.example.com/data')

Basic Usage

src/app/posts/page.tsx
export default async function PostsPage() {
  const response = await fetch('https://api.example.com/posts')
  const posts = await response.json()

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Signature

fetch(url: string | URL, options?: RequestInit): Promise<Response>

Parameters

url

  • Type: string | URL
  • Required: Yes

The URL to fetch. It can be a string or a URL object.

// String URL
await fetch('https://api.example.com/posts')

// URL object
await fetch(new URL('/api/posts', 'https://api.example.com'))

// Relative URL (in Route Handlers)
await fetch('/api/data')

options

  • Type: RequestInit
  • Required: No

Standard fetch options plus rari-specific caching options.

Caching Options

The standard fetch() options are extended with caching controls:

cache

  • Type: 'force-cache' | 'no-store' | 'no-cache' | 'reload'
  • Default: 'force-cache'

Controls how the request interacts with the cache.

// Cache the response (default)
await fetch('https://api.example.com/posts', {
  cache: 'force-cache'
})

// Never cache - always fetch fresh
await fetch('https://api.example.com/posts', {
  cache: 'no-store'
})

rari.revalidate

  • Type: number | false
  • Default: undefined

Set the cache lifetime in seconds. After this time, the cached response expires and a fresh request is made.

// Cache for 60 seconds
await fetch('https://api.example.com/posts', {
  rari: { revalidate: 60 }
})

// Cache for 1 hour
await fetch('https://api.example.com/posts', {
  rari: { revalidate: 3600 }
})

// Disable caching
await fetch('https://api.example.com/posts', {
  rari: { revalidate: false }
})

rari.timeout

  • Type: number
  • Default: 5000 (5 seconds)

Set the request timeout in milliseconds.

// 10 second timeout
await fetch('https://api.example.com/posts', {
  rari: { timeout: 10000 }
})

Request Deduplication

Identical requests made during the same render pass are automatically deduplicated. If multiple components request the same URL with the same options, only one network request is made.

src/app/page.tsx
async function UserProfile({ userId }: { userId: string }) {
  // This fetch is deduplicated if called multiple times
  const user = await fetch(`https://api.example.com/users/`)
    .then(r => r.json())

  return <div>{user.name}</div>
}

export default function Page() {
  return (
    <div>
      {/* Only one request is made, even though UserProfile is rendered twice */}
      <UserProfile userId="123" />
      <UserProfile userId="123" />
    </div>
  )
}

Deduplication only applies to GET requests with identical URLs and options during the same render.

Common Patterns

Static Data Fetching

Fetch data at build time and keep it cached until revalidated or evicted:

src/app/posts/page.tsx
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    cache: 'force-cache' // Default - cached until revalidation/eviction
  }).then(r => r.json())

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Dynamic Data Fetching

Fetch fresh data on every request:

src/app/dashboard/page.tsx
export default async function DashboardPage() {
  const stats = await fetch('https://api.example.com/stats', {
    cache: 'no-store' // Always fetch fresh
  }).then(r => r.json())

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Active users: {stats.activeUsers}</p>
    </div>
  )
}

Revalidated Data Fetching

Fetch data and cache it with a time-to-live:

src/app/posts/page.tsx
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    rari: { revalidate: 60 } // Cache for 60 seconds
  }).then(r => r.json())

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Fetch with Timeout

Set a custom timeout for slow APIs:

src/app/posts/page.tsx
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    rari: {
      revalidate: 60,
      timeout: 10000 // 10 second timeout
    }
  }).then(r => r.json())

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Parallel Data Fetching

Fetch multiple resources in parallel:

src/app/dashboard/page.tsx
export default async function DashboardPage() {
  // Fetch in parallel
  const [users, posts, comments] = await Promise.all([
    fetch('https://api.example.com/users').then(r => r.json()),
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/comments').then(r => r.json()),
  ])

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Users: {users.length}</p>
      <p>Posts: {posts.length}</p>
      <p>Comments: {comments.length}</p>
    </div>
  )
}

Sequential Data Fetching

Fetch data that depends on previous requests:

src/app/users/[id]/page.tsx
import type { PageProps } from 'rari'

export default async function UserPage({ params }: PageProps<{ id: string }>) {
  // First fetch user
  const user = await fetch(`https://api.example.com/users/`)
    .then(r => r.json())

  // Then fetch a specific post using data from the user response
  const favoritePost = await fetch(`https://api.example.com/posts/`)
    .then(r => r.json())

  return (
    <div>
      <h1>{user.name}</h1>
      <h2>Favorite Post</h2>
      <div>
        <h3>{favoritePost.title}</h3>
        <p>{favoritePost.content}</p>
      </div>
    </div>
  )
}

Error Handling

Handle fetch errors gracefully:

src/app/posts/page.tsx
export default async function PostsPage() {
  try {
    const response = await fetch('https://api.example.com/posts')

    if (!response.ok) {
      throw new Error(`HTTP error! status: `)
    }

    const posts = await response.json()

    return (
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>{post.title}</li>
        ))}
      </ul>
    )
  } catch (error) {
    return <div>Failed to load posts</div>
  }
}

POST Requests

Make POST requests with JSON data:

src/app/api/posts/route.ts
import type { RouteHandler } from 'rari'

export const POST: RouteHandler = async (request) => {
  const body = await request.json()

  const response = await fetch('https://api.example.com/posts', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
    cache: 'no-store' // Don't cache POST requests
  })

  const post = await response.json()

  return Response.json(post, { status: 201 })
}

Authentication Headers

Include authentication in requests:

src/app/dashboard/page.tsx
export default async function DashboardPage() {
  const stats = await fetch('https://api.example.com/stats', {
    headers: {
      'Authorization': `Bearer `,
    },
    rari: { revalidate: 60 }
  }).then(r => r.json())

  return (
    <div>
      <h1>Dashboard</h1>
      <p>Active users: {stats.activeUsers}</p>
    </div>
  )
}

Combined Options

Use multiple rari options together:

src/app/posts/page.tsx
export default async function PostsPage() {
  const posts = await fetch('https://api.example.com/posts', {
    headers: {
      'Authorization': `Bearer `,
    },
    rari: {
      revalidate: 300,  // Cache for 5 minutes
      timeout: 8000     // 8 second timeout
    }
  }).then(r => r.json())

  return (
    <ul>
      {posts.map((post: any) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

Caching Behavior

Default Caching

By default, all GET requests are cached:

// These are equivalent
await fetch('https://api.example.com/posts')
await fetch('https://api.example.com/posts', { cache: 'force-cache' })

No Caching

Use cache: 'no-store' to always fetch fresh data:

await fetch('https://api.example.com/posts', {
  cache: 'no-store'
})

Time-Based Caching

Use rari.revalidate to set a cache lifetime in seconds:

// Cache for 1 hour
await fetch('https://api.example.com/posts', {
  rari: { revalidate: 3600 }
})

After the revalidation period expires, the next request will fetch fresh data.

Cache Storage

A two-tier caching system is used:

  1. Request deduplication: Identical requests during the same render are deduplicated in memory
  2. LRU cache: Successful responses are stored in a global LRU cache (max 1000 entries) with TTL support

The cache is powered by Rust for high performance.

Best Practices

Use Appropriate Caching Strategy

Choose the right caching strategy for your data:

  • Static data (rarely changes): Use default caching or cache: 'force-cache'
  • Dynamic data (changes frequently): Use cache: 'no-store'
  • Periodic updates: Use rari.revalidate with an appropriate interval in seconds

Handle Errors Gracefully

Always check response status and handle errors:

const response = await fetch('https://api.example.com/posts')

if (!response.ok) {
  throw new Error(`Failed to fetch: `)
}

const posts = await response.json()

Fetch in Parallel When Possible

Use Promise.all() for independent requests:

// Good - parallel fetching
const [users, posts] = await Promise.all([
  fetch('https://api.example.com/users').then(r => r.json()),
  fetch('https://api.example.com/posts').then(r => r.json()),
])

// Bad - sequential fetching
const users = await fetch('https://api.example.com/users').then(r => r.json())
const posts = await fetch('https://api.example.com/posts').then(r => r.json())

Use Environment Variables for API URLs

Store API URLs in environment variables:

const API_URL = process.env.API_URL || 'https://api.example.com'

const posts = await fetch(`/posts`).then(r => r.json())

Use Built-in Timeout

Use the rari.timeout option instead of AbortController for simpler timeout handling:

// Good - use built-in timeout
const posts = await fetch('https://api.example.com/posts', {
  rari: { timeout: 10000 } // 10 second timeout
}).then(r => r.json())

// Also works - manual AbortController
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000)
const posts = await fetch('https://api.example.com/posts', {
  signal: controller.signal
}).then(r => r.json())
clearTimeout(timeoutId)

Don't Cache POST/PUT/DELETE Requests

Always use cache: 'no-store' for mutations:

await fetch('https://api.example.com/posts', {
  method: 'POST',
  body: JSON.stringify(data),
  cache: 'no-store'
})

Differences from Native fetch

rari's fetch() extends the native Web API with:

  1. Automatic caching: GET requests are cached by default with LRU eviction
  2. Request deduplication: Identical requests during the same render are deduplicated
  3. Time-based revalidation: rari.revalidate option for cache TTL
  4. Built-in timeout: rari.timeout option (default 5 seconds)
  5. Rust-powered performance: Cache operations run in Rust for speed

The core API remains compatible with the standard fetch() API, so existing code works without changes.

Troubleshooting

Cached Data Not Updating

If your data isn't updating as expected:

  • Check if you're using cache: 'force-cache' (the default)
  • Add rari: { revalidate: 60 } to set a cache lifetime in seconds
  • Use cache: 'no-store' for always-fresh data

Request Timing Out

If requests are timing out:

  • Increase the timeout: rari: { timeout: 10000 } (10 seconds)
  • Default timeout is 5 seconds
  • Check if the API server is responding slowly

Request Not Deduplicated

Deduplication only works for:

  • GET requests
  • Identical URLs and options
  • Requests made during the same render pass

POST, PUT, DELETE requests are never deduplicated.

CORS Errors

CORS errors occur when fetching from external APIs in the browser. In rari:

  • Server Components fetch on the server (no CORS issues)
  • Client Components fetch in the browser (subject to CORS)

For Client Components, ensure the API server has appropriate CORS headers or proxy requests through a Route Handler.

  • Routing - Learn about Server Components and Route Handlers
  • Metadata - Configure page metadata