Claude Code SvelteKit Hooks Handle Load (2026)
Claude Code SvelteKit Hooks Handle Load Workflow Tutorial
SvelteKit’s hooks system is one of the most powerful features for handling server-side logic, authentication, and request processing. Combined with the load function workflow, it provides a solid architecture for building modern web applications. In this tutorial, you’ll learn how to effectively work with SvelteKit hooks, the handle function, and the load workflow using Claude Code to accelerate your development.
Understanding SvelteKit Hooks
SvelteKit hooks are server-side functions that intercept requests and responses at various points in the application lifecycle. They live in src/hooks.server.ts (or .js) and allow you to:
- Modify requests before they reach your pages and API routes
- Handle authentication and authorization
- Log requests and responses
- Implement caching strategies
- Handle errors globally
The hooks file exports several optional functions: handle, handleError, handleFetch, and externalFetch. The most commonly used is handle, which processes every incoming request.
Hooks vs Middleware: A Key Distinction
Developers coming from Express or Next.js often equate SvelteKit hooks with middleware, but there are meaningful differences worth understanding before you start building.
| Concept | Express Middleware | Next.js Middleware | SvelteKit Hooks |
|---|---|---|---|
| File location | Defined inline in app.js |
middleware.ts at project root |
src/hooks.server.ts |
| Execution scope | Per-route or global | Edge runtime only | Full Node.js server |
| Data passing | req.locals |
NextResponse headers |
event.locals |
| Type safety | Manual | Partial via types | Full via app.d.ts |
| Form action access | No | No | Yes, shares context |
| SSR data pipeline | No | No | Yes, feeds load functions |
The biggest practical advantage of SvelteKit hooks is the event.locals pipeline. Data you set in handle flows directly into every load function without additional requests or cookie re-reads. This makes authentication and user context nearly free once you set it up correctly.
The Handle Function Detailed look
The handle function is the entry point for all server-side request processing. Here’s a basic example:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
// Log incoming requests
console.log(`[${new Date().toISOString()}] ${event.request.method} ${event.url.pathname}`);
// Process the request
const response = await resolve(event);
// Modify the response if needed
return response;
};
The handle function receives an event object containing all request information, including cookies, headers, and parameters. It returns a response after passing through resolve(event).
The resolve Options Parameter
The resolve function accepts a second argument, an options object, that gives you fine-grained control over how SvelteKit processes the request. This is underused and worth knowing about:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
export const handle: Handle = async ({ event, resolve }) => {
const response = await resolve(event, {
// Transform the response HTML before sending
transformPageChunk: ({ html }) => html.replace(
'<html',
`<html lang="en"`
),
// Filter which headers get serialized into the response
filterSerializedResponseHeaders: (name) => {
return name === 'content-range' || name === 'x-custom-header';
}
});
return response;
};
transformPageChunk is particularly useful for injecting nonces for Content Security Policy, adding custom lang attributes, or inserting environment-specific banners without touching individual page components.
Chaining Multiple Handle Functions with sequence
When your hooks file grows beyond authentication, say you want separate concerns for logging, auth, and rate limiting, the sequence helper from @sveltejs/kit/hooks lets you compose them cleanly:
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import type { Handle } from '@sveltejs/kit';
const logger: Handle = async ({ event, resolve }) => {
const start = Date.now();
const response = await resolve(event);
const duration = Date.now() - start;
console.log(`${event.request.method} ${event.url.pathname}. ${response.status} (${duration}ms)`);
return response;
};
const auth: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session_token');
if (token) {
const user = await verifyToken(token);
if (user) event.locals.user = user;
}
return resolve(event);
};
const rateLimit: Handle = async ({ event, resolve }) => {
const ip = event.getClientAddress();
const allowed = await checkRateLimit(ip);
if (!allowed) {
return new Response('Too Many Requests', { status: 429 });
}
return resolve(event);
};
export const handle = sequence(logger, rateLimit, auth);
The order matters: rateLimit runs before auth so you’re not verifying tokens for requests you’ll reject anyway. logger wraps everything to capture the final status code.
Implementing Authentication in Hooks
One of the most common use cases for hooks is implementing authentication. Here’s a practical example:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { verifyToken } from '$lib/server/auth';
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session_token');
if (token) {
const user = await verifyToken(token);
if (user) {
event.locals.user = user;
}
}
return await resolve(event);
};
To make event.locals.user TypeScript-friendly, declare it in src/app.d.ts:
// src/app.d.ts
declare global {
namespace App {
interface Locals {
user?: {
id: string;
email: string;
role: string;
};
}
}
}
export {};
Route-Level Authorization Guards
Authentication in hooks establishes who the user is. Authorization, whether they’re allowed to access a specific route, should still happen at the load function level. However, you can implement a general guard pattern in hooks for broad protection:
// src/hooks.server.ts
import type { Handle } from '@sveltejs/kit';
import { redirect } from '@sveltejs/kit';
import { verifyToken } from '$lib/server/auth';
// Routes that require authentication
const PROTECTED_PATHS = ['/dashboard', '/settings', '/api/user'];
// Routes that authenticated users shouldn't visit
const AUTH_PATHS = ['/login', '/register'];
export const handle: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session_token');
const path = event.url.pathname;
if (token) {
const user = await verifyToken(token);
if (user) {
event.locals.user = user;
// Redirect authenticated users away from login page
if (AUTH_PATHS.some(p => path.startsWith(p))) {
throw redirect(303, '/dashboard');
}
}
}
// Redirect unauthenticated users away from protected paths
if (!event.locals.user && PROTECTED_PATHS.some(p => path.startsWith(p))) {
throw redirect(303, `/login?redirectTo=${encodeURIComponent(path)}`);
}
return resolve(event);
};
Storing redirectTo in the query string lets your login form redirect users back to where they were trying to go after authentication, which is a significant UX improvement.
Role-Based Access Control
For applications with multiple user roles, you can extend the pattern to enforce role requirements at the hooks level for admin sections:
// src/hooks.server.ts (role-based extension)
const ADMIN_PATHS = ['/admin'];
export const handle: Handle = async ({ event, resolve }) => {
// ... auth logic from above
if (ADMIN_PATHS.some(p => event.url.pathname.startsWith(p))) {
if (!event.locals.user) {
throw redirect(303, '/login');
}
if (event.locals.user.role !== 'admin') {
throw redirect(303, '/403');
}
}
return resolve(event);
};
The SvelteKit Load Workflow
The load function is fundamental to SvelteKit’s data fetching mechanism. It runs on the server during server-side rendering (SSR) and on the client during navigation. Understanding the load workflow is essential for building performant applications.
Understanding Load Function Types
SvelteKit has four types of load files, each with different capabilities and tradeoffs:
| File | Runs on | Has locals |
Has fetch |
Exports actions |
|---|---|---|---|---|
+page.ts |
Server + Client | No | Yes (enhanced) | No |
+page.server.ts |
Server only | Yes | Yes (Node) | Yes |
+layout.ts |
Server + Client | No | Yes (enhanced) | No |
+layout.server.ts |
Server only | Yes | Yes (Node) | Yes |
The key decision is whether you need access to locals (user session, server-only secrets) or whether the data fetch can run on the client. For authenticated pages, always use +page.server.ts. For public data that doesn’t need a server round-trip after the initial load, +page.ts reduces server overhead during client-side navigation.
Page Load Functions
Create a +page.server.ts file to define load functions for your pages:
// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals, params }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const projects = await getUserProjects(locals.user.id);
return {
user: locals.user,
projects
};
};
Layout Load Functions for Shared Data
When multiple pages in a route segment need the same data, like a user profile for a dashboard layout, define it once in +layout.server.ts. Child load functions receive parent data and can extend it:
// src/routes/(app)/+layout.server.ts
import type { LayoutServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
throw redirect(303, '/login');
}
const profile = await db.profile.findUnique({
where: { userId: locals.user.id },
include: { avatar: true, preferences: true }
});
return {
user: locals.user,
profile
};
};
// src/routes/(app)/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ parent, params }) => {
// Access layout data without re-fetching user info
const { user } = await parent();
const [projects, notifications] = await Promise.all([
db.project.findMany({ where: { ownerId: user.id } }),
db.notification.findMany({ where: { userId: user.id, read: false } })
]);
return { projects, notifications };
};
Using await parent() gives you layout data while SvelteKit ensures both loads run in parallel when possible. This is far more efficient than re-fetching the user in every page load function.
Accessing Load Data in Components
In your Svelte component, access the loaded data through the data prop:
<!-- src/routes/dashboard/+page.svelte -->
<script lang="ts">
export let data;
</script>
<h1>Welcome, {data.user.email}</h1>
{#each data.projects as project}
<div class="project">
<h2>{project.name}</h2>
<p>{project.description}</p>
</div>
{/each}
Parallel Data Fetching in Load Functions
A common performance mistake is awaiting database calls sequentially when they have no dependency on each other. Use Promise.all to run independent queries in parallel:
// SLOW: sequential awaits
export const load: PageServerLoad = async ({ locals }) => {
const user = await getUser(locals.user.id); // 45ms
const projects = await getProjects(locals.user.id); // 38ms
const stats = await getStats(locals.user.id); // 52ms
// Total: ~135ms
return { user, projects, stats };
};
// FAST: parallel execution with Promise.all
export const load: PageServerLoad = async ({ locals }) => {
const [user, projects, stats] = await Promise.all([
getUser(locals.user.id),
getProjects(locals.user.id),
getStats(locals.user.id)
]);
// Total: ~52ms (limited by slowest query)
return { user, projects, stats };
};
For more complex cases where some queries depend on others, use a staged approach:
export const load: PageServerLoad = async ({ locals }) => {
// Stage 1: fetch user (needed for everything else)
const user = await getUser(locals.user.id);
// Stage 2: fetch everything that depends on user data in parallel
const [projects, team, billing] = await Promise.all([
getProjects(user.organizationId),
getTeam(user.organizationId),
getBilling(user.organizationId)
]);
return { user, projects, team, billing };
};
Form Actions and Load
SvelteKit combines form actions with the load function, allowing you to handle both data fetching and mutations in the same file:
// src/routes/contacts/+page.server.ts
import type { Actions, PageServerLoad } from './$types';
import { fail } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ locals }) => {
const contacts = await db.contacts.findMany({
where: { userId: locals.user?.id }
});
return { contacts };
};
export const actions: Actions = {
create: async ({ request, locals }) => {
const data = await request.formData();
const email = data.get('email') as string;
if (!email.includes('@')) {
return fail(400, { email, missing: true });
}
await db.contacts.create({
data: { email, userId: locals.user?.id }
});
return { success: true };
}
};
After a successful action, SvelteKit automatically re-runs the load function and updates the page. This means you get optimistic UI patterns without any extra wiring, the contacts list refreshes automatically after you create a new contact.
Handling Action Errors in the Form
Pair form actions with $page.form to display validation errors without losing form state:
<!-- src/routes/contacts/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
export let data;
export let form;
</script>
<form method="POST" action="?/create" use:enhance>
<input
name="email"
type="email"
value={form?.email ?? ''}
class:error={form?.missing}
/>
{#if form?.missing}
<p class="error-text">Please enter a valid email address.</p>
{/if}
<button type="submit">Add Contact</button>
</form>
<ul>
{#each data.contacts as contact}
<li>{contact.email}</li>
{/each}
</ul>
The use:enhance action from $app/forms progressively enhances the form to submit via fetch instead of a full page reload, giving you a SPA-like experience while keeping the server-action architecture.
Connecting Hooks and Load Functions
The real power emerges when hooks and load functions work together. Your authentication logic in handle populates event.locals, which is then accessible in every load function:
// Check authentication in +page.server.ts
export const load: PageServerLoad = async ({ locals }) => {
// User is already available from hooks.handle
const profile = await fetchUserProfile(locals.user.id);
return { profile };
};
This pattern eliminates repetitive authentication checks in every load function, keeping your code DRY and maintainable.
Request Context Flow Diagram
Understanding the full request lifecycle helps you place logic in the right file:
Incoming HTTP Request
↓
hooks.server.ts (handle)
Set event.locals.user
Check rate limits
Add response headers
↓
+layout.server.ts (load)
Read event.locals.user
Fetch shared layout data (profile, nav)
Return layout data
↓
+page.server.ts (load)
Read event.locals.user
Call await parent() for layout data
Fetch page-specific data
Return page data
↓
+page.svelte
Receive merged `data` prop
Render HTML
↓
HTML sent to client
Each step in this chain has access to the same event.locals object, so the user verification done once in handle is available everywhere without redundant database calls.
Using Claude Code to Build SvelteKit Hooks
Claude Code excels at generating SvelteKit boilerplate because the patterns are well-structured and type-safe. Here are practical prompting strategies that work well:
For generating a hooks file from scratch, provide context about your auth strategy:
“Generate a
src/hooks.server.tsfor a SvelteKit app that uses JWT tokens stored in httpOnly cookies. The app has three roles: guest, user, and admin. Admin routes start with/admin. Use thesequencehelper to split logging and auth into separate handle functions.”
For debugging load function type errors, paste the error message and both the app.d.ts and the load file:
“I’m getting a TypeScript error:
Property 'user' does not exist on type 'Locals'. Here are my relevant files: [pasteapp.d.ts] [pastehooks.server.ts]. What’s wrong?”
For refactoring sequential fetches, describe what data you need:
“Refactor this load function to fetch the user, their projects, and their organization settings in parallel. The user must be fetched first because the other queries need
user.organizationId.”
Claude Code handles the Promise.all staging pattern well when you’re explicit about which queries have dependencies.
Practical Tips for Claude Code Development
When working with SvelteKit hooks and load functions using Claude Code, consider these best practices:
-
Type Your Locals Early: Define your
App.Localsinterface inapp.d.tsas soon as you add custom properties. This provides autocomplete and type safety throughout your application. -
Use Hooks for Cross-Cutting Concerns: Authentication, logging, and request modification belong in hooks, not in individual load functions.
-
Use the Load Function for SSR: Use
+page.server.tsload functions when you need server-side data fetching. Client-side fetching with+page.tsis better for truly client-only interactions. -
Handle Errors Gracefully: Export a
handleErrorfunction in your hooks to customize error responses:
export const handleError = ({ error, event }) => {
console.error('Error:', error);
return {
message: 'An unexpected error occurred'
};
};
- Test Your Hooks: Hooks are server-only, so test them with integration tests that make actual HTTP requests to your SvelteKit app.
Testing Hooks and Load Functions
Because hooks and load functions run on the server, you need a different testing approach than unit testing components. The most practical approach is using @sveltejs/kit/test with Vitest’s fetch mocking:
// src/hooks.server.test.ts
import { describe, it, expect, vi } from 'vitest';
import { handle } from './hooks.server';
const mockEvent = (overrides = {}) => ({
cookies: {
get: vi.fn().mockReturnValue(null),
set: vi.fn(),
delete: vi.fn()
},
locals: {},
url: new URL('http://localhost/dashboard'),
request: new Request('http://localhost/dashboard'),
getClientAddress: () => '127.0.0.1',
...overrides
});
describe('handle hook', () => {
it('sets locals.user when valid token is present', async () => {
const event = mockEvent({
cookies: { get: vi.fn().mockReturnValue('valid-token') }
});
const resolve = vi.fn().mockResolvedValue(new Response());
await handle({ event, resolve });
expect(event.locals.user).toBeDefined();
expect(event.locals.user.email).toBe('[email protected]');
});
it('redirects unauthenticated users from protected routes', async () => {
const event = mockEvent();
const resolve = vi.fn().mockResolvedValue(new Response());
const response = await handle({ event, resolve });
expect(response.status).toBe(303);
expect(response.headers.get('location')).toBe('/login?redirectTo=%2Fdashboard');
});
});
Testing load functions directly requires importing them and constructing mock event objects, which is more involved. For load functions, consider using Playwright for end-to-end tests that validate the full data flow from hook to component.
Common Mistakes to Avoid
Awaiting parent() unnecessarily: Calling await parent() before making your own fetches blocks parallel execution. Only call it when you actually need layout data, and ideally after your own queries have started.
Returning non-serializable data: Load functions serialize their return value to pass between server and client. Never return class instances, functions, or circular references, use plain objects and arrays.
Forgetting to handle the unauthenticated case in both hooks and load: Hooks guard route segments, but if your routing doesn’t cover every protected path, individual load functions still need their own checks. Defense in depth applies here.
Over-using +page.server.ts when +page.ts would work: If your data fetch doesn’t require locals or a database connection, using a universal load function means faster client-side navigation because the fetch happens from the browser directly after the first load.
Conclusion
SvelteKit’s hooks system and load workflow form the backbone of server-side request handling in your applications. The handle function in hooks intercepts every request, allowing you to implement authentication, logging, and request modification centrally. Load functions then use this context to fetch data efficiently, whether during server-side rendering or client-side navigation.
The sequence helper, layout load inheritance, and Promise.all parallel fetching are the three techniques that separate good SvelteKit applications from great ones. Combined with Claude Code’s ability to generate and refactor these patterns quickly, you can build solid, secure, and performant SvelteKit applications with significantly less boilerplate than traditional Node.js frameworks require.
By internalizing the request flow, from handle setting locals, through layout loads establishing shared context, to page loads fetching specific data, you’ll know exactly where to place each piece of logic and how to debug it when something goes wrong.
Try it: Paste your error into our Error Diagnostic for an instant fix.
Related Reading
- Claude Code for Load Test Scenario Workflow Tutorial
- How to Handle Chrome Third Party Cookies Blocked in 2026
- Claude Code Algolia GeoSearch Filtering Workflow Tutorial
Built by theluckystrike. More at zovo.one
Find the right skill → Browse 155+ skills in our Skill Finder.