Skip to content

Data Loading

Cloudwerk provides a powerful data loading system using loader() functions that run on the server before rendering.

Export a loader() function from any page or layout to fetch data:

// app/users/page.tsx
import type { PageProps, LoaderArgs } from '@cloudwerk/core';
export async function loader({ context }: LoaderArgs) {
const db = context.env.DB;
const { results: users } = await db.prepare('SELECT * FROM users').all();
return { users };
}
export default function UsersPage({ users }: PageProps & { users: User[] }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}

The LoaderArgs object provides access to:

interface LoaderArgs {
request: Request; // The incoming request
params: Record<string, string>; // URL parameters
context: HonoContext; // Hono context for bindings and middleware state
}
export async function loader({ request }: LoaderArgs) {
// Access query parameters
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') ?? '1');
const limit = parseInt(url.searchParams.get('limit') ?? '10');
// Access headers
const authHeader = request.headers.get('Authorization');
return { page, limit };
}
// app/users/[id]/page.tsx
export async function loader({ params, context }: LoaderArgs) {
const db = context.env.DB;
const user = await db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(params.id)
.first();
return { user };
}

The context is a Hono context that provides access to Cloudflare bindings and middleware-set values:

export async function loader({ context }: LoaderArgs) {
// Database (D1) - access via env bindings
const db = context.env.DB;
const { results: users } = await db.prepare('SELECT * FROM users').all();
// Key-Value store (KV) - access via env bindings
const kv = context.env.CACHE;
const cached = await kv.get('cache-key');
// Object storage (R2) - access via env bindings
const bucket = context.env.UPLOADS;
const file = await bucket.get('uploads/image.png');
// Environment variables
const apiKey = context.env.API_KEY;
// Middleware-set values
const user = context.get('user');
// Cookies
const session = context.req.cookie('session');
return { users };
}

Throw NotFoundError to trigger a 404 response:

import { NotFoundError } from '@cloudwerk/core';
export async function loader({ params, context }: LoaderArgs) {
const db = context.env.DB;
const user = await db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(params.id)
.first();
if (!user) {
throw new NotFoundError('User not found');
}
return { user };
}

Throw RedirectError to redirect:

import { RedirectError } from '@cloudwerk/core';
export async function loader({ context }: LoaderArgs) {
const session = context.req.cookie('session');
if (!session) {
throw new RedirectError('/login');
}
// Validate session and get user from middleware or database
const user = context.get('user');
return { user };
}

Any thrown error will be caught by the nearest error.tsx boundary:

export async function loader({ context }: LoaderArgs) {
const response = await fetch('https://api.example.com/data');
if (!response.ok) {
throw new Error('Failed to fetch data');
}
return { data: await response.json() };
}

Layouts can also have loaders. They execute from parent to child:

// app/layout.tsx
export async function loader({ context }: LoaderArgs) {
const session = context.req.cookie('session');
const user = session ? context.get('user') : null;
return { user };
}
export default function RootLayout({ children, user }: LayoutProps & { user: User | null }) {
return (
<html>
<body>
<Header user={user} />
{children}
</body>
</html>
);
}
// app/dashboard/layout.tsx
import { RedirectError } from '@cloudwerk/core';
export async function loader({ context }: LoaderArgs) {
// This runs AFTER the root layout loader
const user = context.get('user');
if (!user) {
throw new RedirectError('/login');
}
const db = context.env.DB;
const { results: notifications } = await db
.prepare('SELECT * FROM notifications WHERE user_id = ? ORDER BY created_at DESC LIMIT 10')
.bind(user.id)
.all();
return { notifications };
}

When a route has multiple loaders (layouts + page), they execute in parallel when possible:

Root Layout Loader ─┬─> Dashboard Layout Loader ─┬─> Page Loader
│ │
└─> Sidebar Slot Loader ─────┘

Set cache headers in your loader:

export async function loader({ context }: LoaderArgs) {
const db = context.env.DB;
const { results: data } = await db.prepare('SELECT * FROM posts').all();
return {
data,
// Will be merged with response headers
headers: {
'Cache-Control': 'public, max-age=3600',
},
};
}

Use Cloudflare KV for server-side caching:

export async function loader({ context }: LoaderArgs) {
const kv = context.env.CACHE;
const db = context.env.DB;
const cacheKey = 'popular-posts';
// Try cache first
const cached = await kv.get(cacheKey, 'json');
if (cached) {
return { posts: cached };
}
// Fetch fresh data
const { results: posts } = await db
.prepare('SELECT * FROM posts ORDER BY views DESC LIMIT 10')
.all();
// Cache for 5 minutes
await kv.put(cacheKey, JSON.stringify(posts), {
expirationTtl: 300,
});
return { posts };
}

Define your loader return type for full type safety:

import type { PageProps, LoaderArgs } from '@cloudwerk/core';
interface User {
id: string;
name: string;
email: string;
}
interface Post {
id: string;
title: string;
}
interface LoaderData {
user: User;
posts: Post[];
}
export async function loader({ params, context }: LoaderArgs): Promise<LoaderData> {
const db = context.env.DB;
const user = await db
.prepare('SELECT * FROM users WHERE id = ?')
.bind(params.id)
.first();
if (!user) {
throw new NotFoundError('User not found');
}
const { results: posts } = await db
.prepare('SELECT * FROM posts WHERE author_id = ?')
.bind(user.id)
.all();
return { user, posts };
}
export default function UserPage({ user, posts }: PageProps & LoaderData) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
<h2>Posts</h2>
<ul>
{posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}