6.7 KiB
| title | sort | section-id | keywords | description | language |
|---|---|---|---|---|---|
| Data Fetching | 130 | core-concepts | data fetching, SSR, SSG, ISR, server-side rendering, static generation, fetch, async | How to fetch data in Velox using SSR, SSG, ISR, and client-side fetching patterns | en |
Data Fetching
Velox provides multiple data-fetching patterns depending on when and how often your data changes. You can mix strategies freely across routes in the same project.
Server-Side Rendering (SSR)
In SSR mode, data is fetched fresh on every request. Use await directly in the server block of a .velox route:
---
// routes/blog/[slug].velox
import { db } from '$lib/db';
import { NotFound } from 'velox/server';
const { slug } = params;
const post = await db.posts.findBySlug(slug);
if (!post) throw new NotFound();
export const meta = {
title: post.title,
description: post.excerpt,
};
export const config = { render: 'ssr' };
---
<article>
<h1>{post.title}</h1>
<p class="byline">By {post.author} on {post.publishedAt}</p>
<div innerHTML={post.htmlContent} />
</article>
The render: 'ssr' export is optional — SSR is the default for routes that contain await expressions.
Request Context in SSR
In SSR mode, the request object is available in the server block:
const authHeader = request.headers.get('Authorization');
const userAgent = request.headers.get('User-Agent');
const cookieHeader = request.headers.get('Cookie');
Static Site Generation (SSG)
SSG routes are rendered once at build time. They are ideal for content that rarely changes.
---
export const config = { render: 'ssg' };
const features = await fetch('https://api.example.com/features').then(r => r.json());
---
<section>
<h1>Features</h1>
<ul>
{features.map(f => <li>{f.name}: {f.description}</li>)}
</ul>
</section>
Dynamic SSG Routes
For SSG routes with dynamic segments, export a paths function to tell Velox which parameter values to pre-render:
---
import { db } from '$lib/db';
export const config = { render: 'ssg' };
// Called at build time to enumerate all slugs
export async function paths() {
const slugs = await db.posts.findManySlugs();
return slugs.map(slug => ({ slug }));
}
const { slug } = params;
const post = await db.posts.findBySlug(slug);
export const meta = { title: post.title };
---
<article>
<h1>{post.title}</h1>
<div innerHTML={post.htmlContent} />
</article>
Incremental Static Regeneration (ISR)
ISR generates pages statically but revalidates them in the background after a configurable interval:
---
export const config = {
render: 'isr',
revalidate: 3600, // regenerate at most once per hour
};
const products = await db.products.findMany({ orderBy: 'created_at' });
---
<section>
{products.map(p => <ProductCard product={p} />)}
</section>
On a cache miss (first request, or after revalidate seconds), Velox serves the stale page immediately while regenerating the fresh version in the background. The next request gets the fresh version.
Manual Revalidation
Trigger revalidation programmatically (e.g., from a webhook):
// routes/api/revalidate+server.ts
import { revalidatePath, revalidateTag } from 'velox/server';
export const POST = defineHandler(async (req) => {
const { path, secret } = await req.json();
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
await revalidatePath(path);
return Response.json({ revalidated: true });
});
Tag-based revalidation:
// When fetching, attach cache tags:
const data = await fetch('/api/products', {
next: { tags: ['products'] }
});
// Later, invalidate all pages that used the 'products' tag:
await revalidateTag('products');
Client-Side Fetching
For data that must be fresh on the client (user-specific dashboards, real-time data), use client-side fetching in an interactive component:
import { signal, onMount } from 'velox/client';
export default function UserDashboard() {
const stats = signal<DashboardStats | null>(null);
const error = signal<string | null>(null);
const loading = signal(true);
onMount(async () => {
try {
const res = await fetch('/api/dashboard/stats', {
credentials: 'include',
});
if (!res.ok) throw new Error('Failed to fetch stats');
stats.value = await res.json();
} catch (e: any) {
error.value = e.message;
} finally {
loading.value = false;
}
});
return (
<div>
{loading.value && <Spinner />}
{error.value && <ErrorMessage message={error.value} />}
{stats.value && <StatsGrid stats={stats.value} />}
</div>
);
}
Using @velox/query
For production client-side data fetching, @velox/query handles caching, deduplication, background refetch, and stale-while-revalidate:
import { useQuery } from '@velox/query';
export default function ProductList() {
const { data, status, error, refetch } = useQuery({
key: ['products'],
fetcher: () => fetch('/api/products').then(r => r.json()),
staleTime: 30_000,
refetchOnWindowFocus: true,
});
if (status === 'loading') return <Spinner />;
if (status === 'error') return <p>Error: {error.message}</p>;
return (
<ul>
{data.map(p => <li key={p.id}>{p.name}</li>)}
</ul>
);
}
Parallel Data Fetching
Fetch multiple data sources in parallel using Promise.all:
---
const [user, posts, categories] = await Promise.all([
db.users.findById(userId),
db.posts.findByUser(userId),
db.categories.findAll(),
]);
---
Streaming
For pages with slow data, use streaming to send the page shell immediately and stream in the slow parts:
---
import { Suspense } from 'velox';
const fastData = await db.pageConfig.find();
// Slow query wrapped in Suspense — the page shell renders immediately
const slowPostsPromise = db.posts.findMany({ include: { comments: true } });
---
<main>
<h1>{fastData.title}</h1>
<Suspense fallback={<Spinner />}>
<PostList postsPromise={slowPostsPromise} />
</Suspense>
</main>
The <Suspense> boundary renders its fallback immediately while the async data resolves, then streams the final HTML to the client.
Fetch Utilities
Velox wraps the global fetch with several quality-of-life additions in server contexts:
import { fetch } from 'velox/server';
// Automatic base URL resolution for relative paths
const data = await fetch('/api/internal-endpoint');
// Request deduplication — concurrent identical fetches share one in-flight request
const [a, b] = await Promise.all([fetch('/api/same'), fetch('/api/same')]);
// ^ only one HTTP request is made