| name | sveltekit-patterns |
| description | Guides expert-level sveltekit patterns implementation: javascript and frameworks decision frameworks, production-ready patterns, and concrete templates for sveltekit patterns workflows.
Use when the user asks about sveltekit patterns, sveltekit patterns configuration, or javascript best practices for sveltekit projects.
Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.
|
| license | Apache-2.0 |
| metadata | {"author":"foundry-skills","version":"1.0.0","tags":"javascript frameworks web-development backend","category":"web-development","subcategory":"web-development","depends":"","disclaimer":"none","difficulty":"intermediate"} |
SvelteKit Patterns
When to Use
Use this skill when:
- The user asks how to structure data loading in SvelteKit using
+page.server.ts, +page.ts, or +layout.server.ts and needs to understand the difference between server-only and universal loaders
- The user wants to implement form actions, progressive enhancement, or server-side mutations without building a separate REST API layer
- The user needs guidance on SvelteKit's file-based routing system -- including route groups, optional parameters, rest parameters, and parallel route loading
- The user asks about authentication and session management patterns in SvelteKit, including hooks, locals, and middleware-style request handling
- The user wants to optimize a SvelteKit application for production -- including streaming, prerendering strategies, edge deployment, or adapter selection
- The user is confused about when to use a
load function vs a form action vs an API route (+server.ts)
- The user needs to implement real-time features, optimistic UI updates, or complex client-side state that interacts with SvelteKit's store system
Do NOT use this skill when:
- The user is asking about general Svelte component patterns unrelated to routing, loading, or the SvelteKit framework -- check the Svelte component patterns skill instead
- The user needs help with a different meta-framework (Next.js, Nuxt, Remix, Astro) -- those have different mental models and distinct skills
- The user wants to set up a REST or GraphQL API without any UI layer -- that is a backend API design problem, not a SvelteKit routing concern
- The user is asking about deployment infrastructure (Docker, Kubernetes, CDN configuration) beyond selecting a SvelteKit adapter
- The user needs database ORM patterns (Drizzle, Prisma schema design) -- SvelteKit is the transport layer, not the data layer; use database-specific skills for schema design
- The user is asking about general TypeScript configuration or tooling not specific to SvelteKit
- The user wants CSS or design system guidance -- check the styling or component library skills
Process
1. Identify the Architectural Layer Being Asked About
Before recommending any pattern, classify the user's problem into one of SvelteKit's distinct layers:
- Routing layer -- file-system conventions (
+page.svelte, +layout.svelte, +error.svelte, route groups with (groupname), optional params [[param]], rest params [...rest])
- Data loading layer --
load functions in +page.ts (universal), +page.server.ts (server-only), +layout.server.ts (shared across child routes)
- Mutation layer -- form actions in
+page.server.ts using the actions export, progressive enhancement via use:enhance
- API layer --
+server.ts files exposing HTTP endpoints (GET, POST, PUT, DELETE, PATCH) for third-party consumers or client-side fetch calls
- Middleware layer --
src/hooks.server.ts for handle, handleFetch, handleError; src/hooks.client.ts for client-side error handling
- State layer -- Svelte stores,
$page store, invalidate(), invalidateAll(), applyAction() patterns
Ask the user which layer their problem falls in before recommending a solution. A confused question often means conflating two layers -- for example, trying to do session management inside a load function instead of in hooks.server.ts.
2. Clarify the Rendering Strategy
SvelteKit supports four rendering strategies and the correct pattern depends entirely on which strategy is in use:
- SSR (Server-Side Rendering) -- default;
load runs on the server, HTML is sent to the client, then hydrated
- CSR (Client-Side Rendering) -- set
export const ssr = false in the page; load runs in the browser only
- SSG (Static Site Generation) -- set
export const prerender = true; load runs at build time; no server needed at runtime
- Edge rendering -- deploy with an edge adapter (Cloudflare Workers, Vercel Edge); code runs in V8 isolates without Node.js APIs
Ask or infer the rendering strategy from context:
- Marketing or documentation sites → SSG with prerendering
- Authenticated dashboards → SSR with server-only load functions
- High-throughput read APIs with user-specific data → SSR + edge adapter + aggressive caching
- SPAs with complex client state → hybrid (prerender shell, CSR for dynamic sections)
The pattern recommendation changes significantly based on this choice.
3. Diagnose the Data Loading Pattern Needed
Apply this decision tree for data loading:
Does the data require credentials, secrets, or database access?
YES → use +page.server.ts load function (server-only, never sent to client)
NO → Does the data need to work during SSR AND on client-side navigations without
re-fetching from server?
YES → use +page.ts universal load (runs server-side on first load,
client-side on subsequent navigations using fetch)
NO → Is this data shared across many child routes?
YES → use +layout.server.ts or +layout.ts
NO → use +page.ts or +page.server.ts depending on secret access
Key rules for load functions:
- Server
load functions receive { params, url, locals, cookies, fetch, setHeaders, parent } -- never request (that is for actions and +server.ts)
- The
fetch provided to load functions is special -- it forwards cookies, deduplicates requests, and works correctly with relative URLs
parent() retrieves the return value of the nearest parent layout's load function -- use it to access session data without re-fetching
- Use
error(404, 'Not found') from @sveltejs/kit -- not throw new Error() -- to trigger the error page with correct HTTP status codes
- Use
redirect(302, '/login') from @sveltejs/kit for authentication redirects inside load functions
4. Design the Mutation Strategy
SvelteKit provides two approaches for mutations -- form actions and API routes. Apply this decision tree:
Is the mutation triggered by a form submission (or can it be modeled as one)?
YES → Is the mutation initiated from this page's server context?
YES → Use form actions (actions export in +page.server.ts)
NO, from another component → Still use form actions if on the same page;
use +server.ts if it is a shared API
NO → Use +server.ts with the appropriate HTTP method
Form action patterns:
- Default action:
export const actions = { default: async ({ request, locals, cookies }) => {} }
- Named actions:
export const actions = { create: async (...) => {}, update: async (...) => {}, delete: async (...) => {} }
- Access form data with
const data = await request.formData(); const value = data.get('fieldname') as string
- Return
fail(400, { field: 'value', error: 'message' }) for validation errors -- the page re-renders with the form prop populated
- Return
redirect(303, '/new-path') after successful POST (Post/Redirect/Get pattern)
- Use
use:enhance from $app/forms to progressively enhance forms -- the callback receives { formElement, formData, action, cancel, submitter } before submission and returns an async function that receives { result, update } after
Progressive enhancement with custom behavior:
import { enhance } from '$app/forms';
import { applyAction } from '$app/forms';
function submitHandler() {
return async ({ result }) => {
if (result.type === 'redirect') {
await applyAction(result);
} else if (result.type === 'success') {
await applyAction(result);
}
};
}
5. Implement Authentication and Session Patterns
Authentication in SvelteKit belongs in hooks.server.ts, not in individual load functions. The correct pattern:
Step 1 -- Validate session in the handle hook:
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
const auth: Handle = async ({ event, resolve }) => {
const sessionToken = event.cookies.get('session');
if (sessionToken) {
const user = await validateSessionToken(sessionToken);
if (user) {
event.locals.user = user;
event.locals.session = { token: sessionToken };
}
}
return resolve(event);
};
export const handle = sequence(auth);
Step 2 -- Declare locals type in app.d.ts:
declare global {
namespace App {
interface Locals {
user: { id: string; email: string; role: 'admin' | 'user' } | null;
session: { token: string } | null;
}
interface PageData {
user?: App.Locals['user'];
}
}
}
Step 3 -- Access locals in layout load function to populate PageData:
export async function load({ locals }) {
return { user: locals.user };
}
Step 4 -- Protect routes in load functions:
import { redirect } from '@sveltejs/kit';
export async function load({ locals }) {
if (!locals.user) {
redirect(302, '/login');
}
return { user: locals.user };
}
Use route groups (protected) and (public) to separate layouts without affecting the URL structure. Never check authentication in individual +page.server.ts files when a layout can handle it for the entire group.
6. Apply Performance and Caching Patterns
SvelteKit's performance model has several distinct optimization levers:
Streaming with defer-style patterns:
export async function load({ fetch }) {
const user = await getUser();
const recommendations = getRecommendations(user.id);
return {
user,
streamed: { recommendations }
};
}
In the template, use {#await data.streamed.recommendations} blocks to show loading states while the promise resolves.
HTTP cache headers via setHeaders:
export async function load({ setHeaders }) {
setHeaders({
'cache-control': 'max-age=60, stale-while-revalidate=3600'
});
}
Only available in server load functions; ignored in universal load functions during client-side navigation.
Invalidation patterns:
invalidate('app:user') -- invalidate any load functions that called depends('app:user')
invalidateAll() -- re-run all load functions for the current page (expensive, use sparingly)
goto(url, { invalidateAll: true }) -- navigate and invalidate
- Prefer targeted
depends + invalidate over invalidateAll
Preloading:
- Add
data-sveltekit-preload-data="hover" to links for hover-triggered prefetching
- Use
preloadData(url) programmatically from $app/navigation for complex prefetch scenarios
- Set
preload in svelte.config.js under kit.preload to control which asset types are preloaded
7. Structure the Project for Scale
For projects beyond a handful of routes, apply these organizational patterns:
Route groups for layout isolation:
src/routes/
(marketing)/ # Public marketing pages with minimal layout
+layout.svelte
/ # Home page
about/
(app)/ # Authenticated app with sidebar layout
+layout.server.ts # Auth check for entire group
+layout.svelte
dashboard/
settings/
(api)/ # API routes returning JSON
users/
+server.ts
Shared load utilities:
export async function requireUser(locals: App.Locals) {
if (!locals.user) throw redirect(302, '/login');
return locals.user;
}
Type-safe $lib imports:
$lib maps to src/lib -- use it for all shared code
$lib/server/ -- server-only modules; SvelteKit enforces this; importing from client code throws a build error
$lib/components/ -- shared Svelte components
$lib/utils/ -- shared pure utilities safe for both environments
Configuration in svelte.config.js:
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
'$components': 'src/components',
'$server': 'src/lib/server'
},
csrf: { checkOrigin: true },
env: { publicPrefix: 'PUBLIC_' }
}
};
8. Validate and Test the Implementation
SvelteKit-specific testing patterns:
Unit testing load functions with Vitest:
import { describe, it, expect, vi } from 'vitest';
import { load } from './+page.server';
describe('page load', () => {
it('redirects unauthenticated users', async () => {
const event = {
locals: { user: null },
params: {},
url: new URL('http://localhost/dashboard'),
cookies: { get: vi.fn() }
};
await expect(load(event as any)).rejects.toMatchObject({ status: 302 });
});
});
Integration testing with Playwright:
- Use
@playwright/test with SvelteKit's dev server; npm create playwright scaffolds the setup
- Test form submissions end-to-end including validation errors and redirect behavior
- Test with JavaScript disabled to verify progressive enhancement works correctly
Type safety:
- Use the generated
PageData, PageServerLoad, LayoutServerLoad, Actions types from ./$types -- these are auto-generated by the SvelteKit compiler per route
- Never manually type load function return values; rely on inference from the generated types
Output Format
When answering a SvelteKit patterns question, structure the response as follows:
## SvelteKit Pattern Recommendation: [Problem Description]
### Context Assessment
- Rendering strategy: [SSR / SSG / CSR / Edge]
- Architectural layer: [Routing / Loading / Mutation / API / Middleware / State]
- Authentication required: [Yes / No]
### Pattern Decision
[One-paragraph explanation of why this specific pattern applies, referencing SvelteKit's
constraints and the trade-offs of alternatives]
### File Structure
[Show only the relevant files, not the entire project]
src/
routes/
[relevant files with their role noted in comments]
### Implementation
[Complete, copy-pasteable code for each relevant file]
// filename: src/routes/[path]/+page.server.ts
[code block]
// filename: src/routes/[path]/+page.svelte
[code block]
### Type Declarations (if needed)
// filename: src/app.d.ts
[code block]
### Trade-offs
| Concern | This Approach | Alternative |
|------------------|------------------------|--------------------------|
| [concern] | [how this handles it] | [what alternative does] |
### Common Pitfalls for This Pattern
- [specific pitfall 1 with fix]
- [specific pitfall 2 with fix]
Rules
-
Never put secrets or database connections in universal +page.ts load functions. Universal load functions run in the browser during client-side navigation. Database credentials, API keys, and server-only modules ($lib/server/) must only appear in +page.server.ts or +layout.server.ts. Violating this leaks secrets to the client bundle.
-
Never use fetch from the global scope inside server load functions. Always use the fetch argument provided to the load function. The kit-provided fetch handles cookie forwarding, relative URL resolution, and request deduplication that the native fetch does not.
-
Always use error() and redirect() from @sveltejs/kit inside load functions. These are specially recognized thrown values, not errors. Using plain throw new Error() will not set the correct HTTP status code and will not trigger the +error.svelte boundary correctly.
-
Always use status 303 for post-redirect-get after form actions, not 302. The HTTP spec defines 303 as "See Other" specifically for redirecting after a POST. Using 302 can cause browser re-submission warnings and violates REST semantics. SvelteKit's redirect() accepts 301, 302, 303, 307, or 308 -- use 303 for form submissions.
-
Never share mutable state at the module level in server-side code. SvelteKit server modules are not re-instantiated per request. A variable like let currentUser = null at module scope is shared across all concurrent requests. All request-scoped state must be stored in event.locals or returned from load functions.
-
Always declare $lib/server/ imports only in server-side files. SvelteKit enforces this at build time, but the error message can be confusing. If a component or universal load function imports from $lib/server/, the build fails. Keep the boundary explicit: server utilities in $lib/server/, shared safe utilities in $lib/utils/.
-
Never use invalidateAll() as a default after mutations. It re-runs every load function on the page including layout loads. Prefer invalidate('dependency-key') combined with in the specific load function that needs updating. This reduces redundant network requests significantly in nested layouts.
Edge Cases
1. Load Function Depends on Parent Data That May Fail
When a child load function calls parent() and the parent layout's load throws an error(), the child never runs -- the error boundary catches it. However, if the parent returns null for optional data (like a user that may not be logged in), the child must handle the null case:
export async function load({ parent }) {
const { user } = await parent();
const publicData = await getPublicData();
const personalizedData = user ? await getPersonalizedData(user.id) : null;
return { publicData, personalizedData };
}
Never assume parent data is populated. Layout files in the (marketing) group may not set user at all, making locals.user undefined rather than null. Use nullish coalescing consistently.
2. Form Actions With File Uploads
SvelteKit's request.formData() supports file uploads, but the file object is a Web API File, not a Node.js Buffer. The handling differs by adapter:
export const actions = {
upload: async ({ request }) => {
const formData = await request.formData();
const file = formData.get('avatar') as File;
if (!(file instanceof File) || file.size === 0) {
return fail(400, { error: 'No file provided' });
}
if (file.size > 5 * 1024 * 1024) {
return fail(400, { error: 'File too large' });
}
const buffer = Buffer.from(await file.arrayBuffer());
await saveFile(buffer, file.name, file.type);
}
};
The file.arrayBuffer() approach works across Node, Edge, and Cloudflare Workers adapters. Avoid Node-specific Readable.from() patterns if cross-adapter compatibility is needed.
3. Race Conditions With Navigation and Invalidation
When a user navigates rapidly or submits a form while a previous load is still in flight, SvelteKit cancels previous navigation promises automatically for page loads, but invalidate() calls issued programmatically can stack. If a component calls invalidate() inside a reactive $: block or a onMount timer, it can trigger cascading re-loads:
$: if (someCondition) {
invalidate('app:data');
}
let hasInvalidated = false;
$: if (someCondition && !hasInvalidated) {
hasInvalidated = true;
invalidate('app:data');
}
For polling patterns, use setInterval with invalidate but clear the interval in onDestroy to prevent memory leaks and stale invalidations after navigation.
4. SSR + Client State Hydration Mismatch
When a +page.svelte uses browser-only APIs (localStorage, window, document) in a way that produces different output during SSR vs. client render, SvelteKit will log a hydration mismatch warning and the page may flicker:
<script>
import { browser } from '$app/environment';
// WRONG -- runs during SSR, window is undefined
let theme = window?.localStorage.getItem('theme') ?? 'light';
// CORRECT -- deferred to browser only
let theme = 'light'; // safe default for SSR
onMount(() => {
theme = localStorage.getItem('theme') ?? 'light';
});
</script>
Use browser from $app/environment for conditional execution. For initial render of user preferences (theme, locale), pass them via cookies read in the server load function so SSR and client render agree from the start. Cookies are available in load via event.cookies.get() and can be set in actions or the handle hook.
5. API Routes vs Form Actions for SPA-Style Interactions
When a Svelte component that is not a page (e.g., a modal dialog or a sidebar component) needs to submit data, it cannot use a page's form actions directly because form actions are scoped to the route that contains them. The options are:
Option 1 -- Fetch to a +server.ts API route:
export async function POST({ request, locals }) {
if (!locals.user) return new Response(null, { status: 401 });
const body = await request.json();
const comment = await createComment(body, locals.user.id);
return Response.json(comment, { status: 201 });
}
Option 2 -- Relocate the form action to a parent layout:
If the component appears on multiple pages, put the action in a shared layout's +layout.server.ts and post to ?/actionName from anywhere under that layout.
Option 3 -- Use a dedicated form route as a POST target:
Create a route at /actions/[entity] that only handles POST and immediately redirects. This is the "action routes" pattern and avoids polluting layout files.
Never put authentication logic in +server.ts routes redundantly when hooks.server.ts already validates the session into locals. Just check locals.user -- the hook has already done the work.
6. Adapter Selection for Edge Cases
| Deployment Target | Adapter | Key Constraints |
|---|
| Vercel (serverless) | @sveltejs/adapter-vercel | 10s default timeout; use export const config = { runtime: 'edge' } per route for V8 isolates |
| Cloudflare Workers | @sveltejs/adapter-cloudflare | No Node.js APIs; use platform.env for KV/D1; 128MB memory limit |
| Static hosting (Netlify CDN, S3) | @sveltejs/adapter-static | All routes must be prerenderable; no server load functions at runtime |
| Self-hosted Node.js | @sveltejs/adapter-node | Full Node.js access; must configure reverse proxy for HTTPS |
| Auto-detect | @sveltejs/adapter-auto | Detects Vercel/Netlify/Cloudflare at build time; falls back to Node |
When targeting Cloudflare Workers, the platform object is injected into event.platform -- access Cloudflare-specific bindings like event.platform.env.MY_KV only in server load functions and +server.ts files. Declare the type in app.d.ts under App.Platform.
7. Handling Cookies Across Subdomains and OAuth Flows
SvelteKit's cookies.set() API defaults to path: '/' and httpOnly: true, but several OAuth scenarios require specific cookie settings:
event.cookies.set('session', token, {
path: '/',
httpOnly: true,
secure: !dev,
sameSite: 'lax',
maxAge: 60 * 60 * 24 * 30
});
For multi-subdomain auth (e.g., app.example.com and admin.example.com sharing a session):
- Set
domain: '.example.com' on the cookie
- Ensure the session validation in
hooks.server.ts works across subdomains
- Note that
sameSite: 'strict' will block the cookie when redirecting back from an OAuth provider -- always use 'lax' for authentication cookies
Example
Input: "I'm building a SvelteKit app where users can manage their projects. I need a dashboard page that shows a list of the user's projects, a form to create a new project, and I want the list to update immediately after creation without a full page reload. The user must be authenticated to see this page."
Output:
Context Assessment
- Rendering strategy: SSR (authenticated dashboard with user-specific data)
- Architectural layer: Loading (server load for project list) + Mutation (form action for creation) + Progressive Enhancement (optimistic list update)
- Authentication required: Yes -- session validated in
hooks.server.ts, enforced in layout
Pattern Decision
This uses a server-only load function to fetch projects (requires database access), a named form action to create projects, and use:enhance with a custom callback to append the new project to the list immediately without waiting for the full page re-load. The authentication gate lives in the route group layout, not the page itself, keeping the page load function focused on data.
File Structure
src/
hooks.server.ts -- session validation → event.locals.user
app.d.ts -- App.Locals and App.PageData types
lib/
server/
db.ts -- database access (server-only)
projects.ts -- project query/mutation functions
routes/
(app)/
+layout.server.ts -- auth guard for all (app) routes
+layout.svelte -- app shell with nav
dashboard/
+page.server.ts -- load projects + form actions
+page.svelte -- project list + create form
Implementation
declare global {
namespace App {
interface Locals {
user: { id: string; email: string; name: string } | null;
}
interface PageData {
user?: App.Locals['user'];
}
}
}
export {};
import type { Handle } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import { validateSession } from '$lib/server/auth';
const auth: Handle = async ({ event, resolve }) => {
const token = event.cookies.get('session');
if (token) {
const user = await validateSession(token);
event.locals.user = user ?? null;
} else {
event.locals.user = null;
}
return resolve(event);
};
export const handle = sequence(auth);
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
export const load: LayoutServerLoad = async ({ locals }) => {
if (!locals.user) {
redirect(302, '/login');
}
return { user: locals.user };
};
import { fail, redirect } from '@sveltejs/kit';
import { getProjectsByUser, createProject } from '$lib/server/projects';
import type { PageServerLoad, Actions } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
const projects = await getProjectsByUser(locals.user!.id);
return { projects };
};
export const actions: Actions = {
create: async ({ request, locals }) => {
const formData = await request.formData();
const name = (formData.get('name') as string)?.trim();
const description = (formData.get('description') as string)?.trim() ?? '';
if (!name || name.length < ) {
(, {
name,
description,
:
});
}
(name. > ) {
(, {
name,
description,
:
});
}
existing = (locals.!.);
(existing.( p..() === name.())) {
(, {
name,
description,
:
});
}
project = ({
name,
description,
: locals.!.
});
{ : , project };
}
};
<!-- filename: src/routes/(app)/dashboard/+page.svelte -->
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData, ActionData } from './$types';
export let data: PageData;
export let form: ActionData;
// Local copy of projects so we can append optimistically
let projects = data.projects;
let submitting = false;
let formEl: HTMLFormElement;
function handleCreate() {
submitting = true;
return async ({ result, update }) => {
submitting = false;
if (result.type === 'success' && result.data?.project) {
// Optimistically append without re-running load
projects = [...projects, result.data.project];
formEl.reset();
} else {
// Let SvelteKit apply the failure result (populates `form` prop)
await update();
}
};
}
</script>
<h1>Dashboard</h1>
<p>Welcome, {data.user?.name}</p>
<section aria-label="Your projects">
<h2>Projects ({projects.length})</h2>
{#if projects.length === 0}
<p>No projects yet. Create your first one below.</p>
{:else}
<ul>
{#each projects as project (project.id)}
<li>
<a href="/dashboard/{project.id}">{project.name}</a>
{#if project.description}
<p>{project.description}</p>
{/if}
</li>
{/each}
</ul>
{/if}
</section>
<section aria-label="Create a new project">
<h2>New Project</h2>
{#if form?.error}
<p role="alert" style="color: red">{form.error}</p>
{/if}
<form
method="POST"
action="?/create"
use:enhance={handleCreate}
bind:this={formEl}
>
<label>
Project Name
<input
type="text"
name="name"
value={form?.name ?? ''}
required
minlength="2"
maxlength="100"
disabled={submitting}
aria-invalid={form?.error ? 'true' : undefined}
/>
</label>
<label>
Description (optional)
<textarea
name="description"
disabled={submitting}
>{form?.description ?? ''}</textarea>
</label>
<button type="submit" disabled={submitting}>
{submitting ? 'Creating…' : 'Create Project'}
</button>
</form>
</section>
Trade-offs
| Concern | This Approach | Alternative |
|---|
| Optimistic update | Append to local projects array on success | Call invalidateAll() -- simpler but causes full re-load |
| Auth enforcement | Route group layout load function | Per-page auth check -- duplicates logic, easy to forget |
| Form errors | fail() return + form prop binding | Separate API route + client fetch -- more complex, loses progressive enhancement |
| Type safety | PageServerLoad, Actions, PageData from ./$types | Manual types -- diverge from actual return shapes silently |
Common Pitfalls for This Pattern
- Not resetting the form after success: Calling
formEl.reset() is required after optimistic append; without it the inputs retain their values even though the form data was submitted.
- Using
invalidateAll() inside use:enhance callback by default: This re-runs the layout's load function too, causing a database round-trip for the user object that was already loaded. Use targeted invalidation or the optimistic pattern shown above.
- Returning the new project from the action but not declaring the return type: The auto-generated
ActionData type from ./$types infers result.data correctly only if TypeScript can see the return value. Avoid as any casts in the action return.
- Forgetting
aria-invalid on fields: Server-side validation errors that re-render the form without ARIA attributes fail accessibility audits. Bind aria-invalid to the presence of a field-specific error.
- Route group folder naming affecting URL:
(app) is a route group and does NOT appear in the URL. /dashboard routes to src/routes/(app)/dashboard/+page.svelte correctly. Do not add visible segment folders expecting them to be invisible.