Data Loading
Cloudwerk provides a powerful data loading system using loader() functions that run on the server before rendering.
Loader Basics
Section titled “Loader Basics”Export a loader() function from any page or layout to fetch data:
// app/users/page.tsximport 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> );}Loader Arguments
Section titled “Loader Arguments”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}Using Request Data
Section titled “Using Request Data”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 };}Using Route Parameters
Section titled “Using Route Parameters”// app/users/[id]/page.tsxexport 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 };}Using Context
Section titled “Using Context”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 };}Error Handling
Section titled “Error Handling”Not Found Errors
Section titled “Not Found Errors”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 };}Redirects
Section titled “Redirects”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 };}Generic Errors
Section titled “Generic Errors”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() };}Layout Loaders
Section titled “Layout Loaders”Layouts can also have loaders. They execute from parent to child:
// app/layout.tsxexport 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.tsximport { 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 };}Parallel Data Loading
Section titled “Parallel Data Loading”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 ─────┘Caching
Section titled “Caching”Response Caching
Section titled “Response Caching”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', }, };}KV Caching
Section titled “KV Caching”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 };}TypeScript
Section titled “TypeScript”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> );}Next Steps
Section titled “Next Steps”- Forms and Actions - Handle form submissions
- Database Guide - Work with Cloudflare D1
- Authentication - Protect your routes