Why These Patterns Matter
SvelteKit takes care of a lot of the low-level mechanics of building web applications: routing, server-side rendering, client-side hydration, code splitting. It's opinionated in the right ways, which means you can be productive quickly without making endless configuration decisions.
But having good defaults doesn't mean you can ignore architecture. The projects that stay maintainable over time—the ones where you can still navigate the codebase six months later—are the ones that follow consistent patterns. This guide covers the patterns we've seen work best across hundreds of SvelteKit projects.
The Library Pattern: Keep Truth Separate from Presentation
One of the most common mistakes in SvelteKit projects is scattering business logic across route files. It starts innocently: you need a utility function, so you write it at the top of +page.svelte. Then you need it in another page, so you copy it. Before long, the same logic lives in five places, and they've all drifted slightly out of sync.
The fix is to treat src/lib as the single source of truth for everything reusable, and treat your routes folder as purely presentation—glue code that connects data to views.
In practice, this means:
UI components go inlib/components. These are your reusable building blocks: buttons, cards, modals, form inputs. They shouldn't contain business logic—they receive data as props and emit events when the user interacts with them.
Business logic goes in lib/server or lib/utils. Functions like calculateTax() or validateEmail() or formatDate() belong here. When you need them, you import them. There's exactly one implementation, so bugs get fixed in one place.
Routes are thin. Your +page.svelte files should primarily be responsible for composing components and connecting them to your data loading functions. If your route file is hundreds of lines long, that's a sign that logic that belongs in lib has leaked into your presentation layer.
This pattern pays dividends as projects grow. When someone new joins the team, they can understand the codebase faster because there's a clear separation between "where the logic lives" and "where the UI lives."
Server-Side Data Loading: Don't Fetch in onMount
One of the most powerful features of SvelteKit is its load function. When a user navigates to a page, the load function runs on the server (for initial page loads) or the client (for client-side navigation), and the data it returns is available to your page component before it renders.
This is fundamentally different from the old pattern of fetching data in onMount:
The difference matters for two reasons.
First, no loading spinners. When you fetch data inonMount, the user first sees an empty page (or a loading skeleton), then the data arrives, then the page fills in. With server-side loading, the HTML that arrives already contains the data. There's no flash of empty content, no layout shift as data loads in.
Second, better SEO. Search engine crawlers are getting better at JavaScript, but they still prefer pages that work without it. Server-rendered HTML is fully visible to crawlers and social media preview bots, which means your content gets indexed properly.
The practical advice: if your page needs data, load it in +page.server.js or +page.js. Reserve onMount for things that genuinely can only happen on the client, like initializing maps or setting up WebSocket connections.
Type Safety Without TypeScript: JSDoc Annotations
There's a common misconception that you need TypeScript to get type safety in JavaScript. You don't. SvelteKit has excellent support for JSDoc annotations, which give you type checking and autocomplete without changing your file extensions or adding a build step.
The syntax is simple. At the top of your load function, you add a comment that describes its type:
/ @type {import('./$types').PageServerLoad} */
export async function load({ fetch, params }) {
const post = await fetchPost(params.slug);
return { post };
}
That single line gives your editor everything it needs. It knows what arguments your function receives, what fields are available on params, what type fetch returns. You get red squiggles when you make type errors, and autocomplete suggestions that actually match your data.
The benefit of JSDoc over TypeScript is simplicity. Your code is standard JavaScript that runs anywhere. You don't need a compile step. You can copy a function into a browser console and it just works. But you still get the safety net of type checking during development.
Progressive Enhancement: Forms That Work Without JavaScript
The web platform has forms built in. They've worked since the early 1990s. You can submit a form, send data to a server, and get a response back—all without a single line of JavaScript.
SvelteKit embraces this with its use:enhance directive. You start with a standard HTML form that works even if JavaScript fails to load:
Then you add use:enhance to progressively enhance it into a smooth, SPA-like experience:
With use:enhance, the form submission happens via AJAX. The page doesn't reload. You can show loading states, handle errors gracefully, and update the UI smoothly. But if JavaScript fails—which it does more often than you'd think, especially on flaky mobile connections—the form still works because it falls back to standard HTML behavior.
The rule of thumb: always start with native . Get that working first. Then add use:enhance. If you start with onclick handlers and fetch calls, you're fighting the platform instead of using it.
Stores: Less Is More
Svelte stores are wonderfully simple to use. You create a store, subscribe to it, update it—all in a few lines of code. This simplicity is also their danger.
Because stores are so easy, it's tempting to use them for everything. Need to pass data between components? Store. Need to remember a filter selection? Store. Need to track form values? Store.
The problem is that stores are global state, and global state has costs. It's harder to reason about. It creates hidden dependencies between components. It makes testing more difficult.
Most state doesn't need to be global:
Widget-specific state stays local. If you're tracking whether a dropdown is open, that's a local variable inside the component. It doesn't affect anything else. State that should survive page refresh belongs in the URL. Filter selections, sort orders, pagination—these should be query parameters (?filter=active&sort=date). This makes your pages bookmarkable and shareable.
Only truly global state goes in stores. The user's session. The app's theme preference. The shopping cart in an e-commerce site. Things that are genuinely used across the entire application.
When you're tempted to reach for a store, ask yourself: is this really global, or am I just trying to avoid prop drilling? If it's the latter, consider whether restructuring your components might be a cleaner solution.
Structure Matters More Than Syntax
SvelteKit lets you write less code than most frameworks. That's a genuine advantage—less code means fewer bugs, faster loading, easier maintenance. But "less code" doesn't mean "no structure."
The patterns in this guide aren't about adding complexity. They're about putting things in predictable places so that future-you (and future-teammates) can find them. When your lib folder contains all reusable logic, when your routes are thin presentation layers, when your data loading happens in load functions, when your forms work without JavaScript—the codebase stays navigable as it grows.
Good structure is invisible when it's working. You notice it only when it's missing: when you can't find where a function is defined, when changes break things in unexpected places, when you're afraid to refactor because you don't understand the implications.
Build with structure from the start. Your future self will thank you.
Start your SvelteKit project