fetch
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
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.
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
- Request deduplication: Identical requests during the same render are deduplicated in memory
- 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.revalidatewith 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:
- Automatic caching: GET requests are cached by default with LRU eviction
- Request deduplication: Identical requests during the same render are deduplicated
- Time-based revalidation:
rari.revalidateoption for cache TTL - Built-in timeout:
rari.timeoutoption (default 5 seconds) - 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:
GETrequests- 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.