Why do 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 stays opinionated in the right ways, which means you can be productive quickly without making endless configuration decisions.
But good defaults do not replace 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. Think of the codebase as a workshop. Every tool has a labeled drawer. This guide covers the drawer labels we have seen work best, drawn from the official docs at each step.
Try it: Deploy Checklist — which keeps structure checks in the shipping routine.
How do you keep truth separate from presentation? #
The most common mistake 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 all five have 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 presentation. Glue code that connects data to views.
In practice, this means three drawers.
UI components live in lib components #
These are your reusable building blocks: buttons, cards, modals, form inputs. They receive data as props and emit events when the user interacts with them. They do not fetch. They do not own business rules. As stated in the routing docs (https://svelte.dev/docs/kit/routing), pages receive data from load functions through the data prop, which keeps the component tree fed from above instead of reaching out sideways.
Business logic lives in lib server or lib utils #
Functions like calculateTax() or validateEmail() or formatDate() belong here. When you need them, you import them. There is exactly one implementation, so bugs get fixed in one place. Server only code goes behind $lib/server or in +page.server.js so secrets and database clients never leak to the browser. The auth best practices page (https://svelte.dev/docs/kit/auth) pushes the same shape: handle sessions and secrets on the server, expose only what the page needs.
// lib/utils/pricing.js, one implementation, imported everywhere
export function totalWithTax(subtotal, rate = 0.2) {
if (subtotal < 0) throw new Error('negative subtotal');
return Math.round(subtotal * (1 + rate) * 100) / 100;
}Routes stay thin #
Your +page.svelte files compose components and connect them to data loading functions. If a route file runs to hundreds of lines, logic that belongs in lib has leaked into the presentation layer. When someone new joins, they learn the codebase faster because there is a clear split between where logic lives and where the UI lives.
| Habit | Put it here | Why it helps | When not to |
|---|---|---|---|
| Reusable truth | src/lib | One fix, every route | Route only glitch |
| Data load | load on server | HTML arrives with data | Maps or sockets need client |
| Forms | form actions plus enhance | Works without JS | Custom fetch niche |
| Shared state | URL or local | Bookmarkable and testable | Session or theme needs store |
When should you load data on the server? #
One of the most capable features of SvelteKit is the load function. When a user navigates to a page, the load function runs on the server for initial loads or on the client for client side navigation, and the data it returns is available to the page component before it renders.
This differs from the old pattern of fetching data in onMount:
The difference matters for two reasons.
First, no loading spinners. When you fetch data in onMount, the user first sees an empty page, then the data arrives, then the page fills in. With server side loading, the HTML that arrives already contains the data. No flash of empty content. No layout shift as data loads in.
Second, better SEO. Crawlers keep improving at JavaScript, but they still prefer pages that work without it. Server rendered HTML is fully visible to crawlers and social preview bots, which means content gets indexed properly.
The practical advice: if a page needs data, load it in +page.server.js or +page.js. As stated in the loading data docs (https://svelte.dev/docs/kit/load), server load functions always run on the server and suit database or secret access, while universal load functions run on both and suit public fetches. Reserve onMount for things that genuinely can only happen on the client, like initializing maps or setting up WebSocket connections.
// +page.server.js, secrets and DB stay on the server
/** @type {import('./$types').PageServerLoad} */
export async function load({ params }) {
const post = await db.getPost(params.slug);
return { post };
}How do you get type safety without TypeScript? #
There is a common misconception that you need TypeScript to get type safety in JavaScript. You do not. SvelteKit generates $types modules with excellent support for JSDoc annotations, which give you type checking and autocomplete without changing file extensions or adding a build step.
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 sit 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. No compile step. You can copy a function into a browser console and it just works. And you still get the safety net of type checking during development.
How do forms work without JavaScript? #
The web platform has forms built in. They have 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 form actions plus the use:enhance directive. As stated in the form actions docs (https://svelte.dev/docs/kit/form-actions), a +page.server.js file exports actions that receive POSTs from plain HTML forms, and use:enhance progressively improves them into smooth AJAX submissions. You start with a standard HTML form that works even if JavaScript fails to load:
<form method="POST" action="?/submitFeedback">
<textarea name="message"></textarea>
<button>Send</button>
</form>Then you add use:enhance for the smooth single page experience:
<form method="POST" action="?/submitFeedback" use:enhance>
<textarea name="message"></textarea>
<button>Send</button>
</form>With use:enhance, the submission happens without a full page reload. You can show loading states, handle errors gracefully, and update the UI smoothly. But if JavaScript fails, which happens more often than we admit on flaky mobile connections, the form still works because it falls back to standard HTML behavior.
The rule of thumb: always start with native <form method="POST">. Get that working first. Then add use:enhance. If you start with onclick handlers and fetch calls, you fight the platform instead of using it. Server validation stays mandatory either way. The client is a convenience, never the guard.
Why is less more with stores? #
Svelte stores are wonderfully simple to use. You create a store, subscribe to it, update it, all in a few lines. That simplicity is also their danger.
Because stores are so easy, temptation says use them for everything. Pass data between components? Store. Remember a filter selection? Store. Track form values? Store.
The problem is that stores are global state, and global state has costs. Harder to reason about. Hidden dependencies between components. More difficult testing. As stated in the state management docs (https://svelte.dev/docs/kit/state-management), SvelteKit leans on URL, load data, and component state first, with shared modules for the rest.
Most state does not need to be global.
Widget specific state stays local #
Tracking whether a dropdown is open is a local variable inside the component. It affects nothing else. Keep it there.
Shareable state belongs in the URL #
Filter selections, sort orders, pagination. These should be query parameters such as ?filter=active&sort=date. Pages become bookmarkable and shareable. Back buttons behave. Support links carry context for free.
Only truly global state goes in stores #
The user session. The app theme preference. The shopping cart in an e commerce site. Things genuinely used across the entire application. When tempted to reach for a store, ask whether the state is really global or you just want to avoid prop drilling. If it is the latter, restructuring components is usually the cleaner fix.
Which performance habits compound? #
The performance best practices page (https://svelte.dev/docs/kit/performance) reads like a checklist earned from real apps. A few habits pay for themselves.
Preload data for links users are likely to visit so navigation feels instant. Compress and size images instead of shipping camera originals. Keep JavaScript bundles lean so phones parse less. Cache aggressively where correctness allows. None of these replace the structural patterns above. They multiply them.
Measure before and after. Ship the faster version, then confirm the metric that matters moved. Speed work earns its keep in behavior, not scores.
Why does structure matter more than syntax? #
SvelteKit lets you write less code than most frameworks. Less code means fewer bugs, faster loading, easier maintenance. But less code does not mean no structure.
The patterns in this guide are not about adding complexity. They put things in predictable places so that future you and future teammates can find them. When your lib folder contains all reusable logic, when routes are thin presentation layers, when data loading happens in load functions, when forms work without JavaScript, the codebase stays navigable as it grows.
Good structure is invisible when it works. You notice it only when it is missing: when you cannot find where a function is defined, when changes break things in unexpected places, when you fear refactoring because you cannot see the implications.
Build with structure from the start. Your future self will thank you.
What are the trade-offs? #
Structure pays compound interest. It also charges upfront.
| Where the habits win | Where they cost |
|---|---|
| Truth in src/lib with thin routes keeps logic findable, on routing (https://svelte.dev/docs/kit/routing) and load (https://svelte.dev/docs/kit/load) patterns | Small demos carry scaffolding they never use. One route with no reuse does not need layers |
| Server loads plus form actions (https://svelte.dev/docs/kit/form-actions) work before JavaScript and keep auth sane (https://svelte.dev/docs/kit/auth) | Progressive enhancement thinking slows the first pass. The return lands months later |
| Lean stores (https://svelte.dev/docs/kit/state-management) plus measured performance (https://svelte.dev/docs/kit/performance) keep phones fast | Discipline needs enforcing: every shortcut compounds too, in the other direction |
Pick the alternative, a loose single file spike, only for throwaway demos with no future. Anything longer-lived earns the layers.
Who this is for (and who should skip it) #
This guide helps builders who want SvelteKit projects that stay navigable after six months. If you prefer lib as truth and routes as thin presentation plus server loads for page data, the habits here keep debt low.
Skip strict layering if you are spiking a throwaway demo with one route and no reuse. For anything that ships and grows, keep business logic out of plus page svelte and put it in lib.
- Best for developers structuring SvelteKit truth in lib and presentation in routes.
- Best for startups keeping server loads, forms, and stores consistent as apps grow.
- Best for freelancers shipping maintainable client apps with type safety and performance habits.
What we learned building this #
BYOB generates every project as a SvelteKit app with filesystem routing, server load functions, and form actions. Keeping reusable truth in a shared lib folder and routes thin is not style advice here, it is how the generator avoids invention per prompt and keeps output predictable. Live at https://byob.studio/blog/how-byob-uses-sveltekit that doc returns 200 and walks the lib plus routes split with the compilation pipeline.