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 } 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>
)
}

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
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()
return { user }
}

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 }
}

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 }
}

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 }
}

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 }
}

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
import { 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.tsx
import { 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 }
}

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:

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',
},
}
}

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 }
}

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>
)
}