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 } from '@cloudwerk/core'import { db } from '@cloudwerk/core/bindings'
export async function loader() { 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.tsximport { params } from '@cloudwerk/core/context'import { db } from '@cloudwerk/core/bindings'
export async function loader() { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first()
return { user }}Using Importable Bindings
Section titled “Using Importable Bindings”Import bindings directly from @cloudwerk/core/bindings for clean, ergonomic access:
import { db, kv, r2 } from '@cloudwerk/core/bindings'import { getUser } from '@cloudwerk/auth'
export async function loader() { // Database (D1) const { results: users } = await db.prepare('SELECT * FROM users').all()
// Key-Value store (KV) const cached = await kv.get('cache-key')
// Object storage (R2) const file = await r2.get('uploads/image.png')
// Get authenticated user const user = getUser()
return { users, cached, user }}Using Context Parameter
Section titled “Using Context Parameter”Alternatively, you can access bindings through the context parameter. This is useful when you need access to request-specific data or middleware-set values:
export async function loader({ context }: LoaderArgs) { // Database (D1) via context const db = context.env.DB const { results: users } = await db.prepare('SELECT * FROM users').all()
// Key-Value store (KV) via context const kv = context.env.CACHE const cached = await kv.get('cache-key')
// Middleware-set values const tenant = context.get('tenant')
// Cookies const theme = context.req.cookie('theme')
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'import { params } from '@cloudwerk/core/context'import { db } from '@cloudwerk/core/bindings'
export async function loader() { 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'import { getUser, isAuthenticated } from '@cloudwerk/auth'
export async function loader() { if (!isAuthenticated()) { throw new RedirectError('/login') }
const user = getUser() 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.tsximport { getUser } from '@cloudwerk/auth'
export async function loader() { const user = getUser() 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'import { getUser } from '@cloudwerk/auth'import { db } from '@cloudwerk/core/bindings'
export async function loader() { // This runs AFTER the root layout loader const user = getUser() if (!user) { throw new RedirectError('/login') }
const { results: notifications } = await db .prepare('SELECT * FROM notifications WHERE user_id = ? AND read = 0') .bind(user.id) .all()
return { notifications: 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:
import { db } from '@cloudwerk/core/bindings'
export async function loader() { 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:
import { kv, db } from '@cloudwerk/core/bindings'
export async function loader() { 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 } from '@cloudwerk/core'import { NotFoundError } from '@cloudwerk/core'import { params } from '@cloudwerk/core/context'import { db } from '@cloudwerk/core/bindings'
interface User { id: string name: string email: string}
interface Post { id: string title: string}
interface LoaderData { user: User posts: Post[]}
export async function loader(): Promise<LoaderData> { const user = await db .prepare('SELECT * FROM users WHERE id = ?') .bind(params.id) .first<User>()
if (!user) { throw new NotFoundError('User not found') }
const { results: posts } = await db .prepare('SELECT * FROM posts WHERE author_id = ?') .bind(user.id) .all<Post>()
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