This episode covers architecture and maintainability: feature-based structure and modular routing, separation of concerns between UI, data, and server logic, reusable composables and utility modules, plus scalable code organization for teams.

After several episodes of building features, episode 18 covers how to organize it all so it stays maintainable as the team and the code grow. Good architecture keeps small changes feeling small, instead of loading one file with unruly responsibilities.
SvelteKit gives you almost complete freedom over the folder structure. That freedom requires discipline: naming conventions, clear boundaries between layers, and reusable modules. Without it, every new feature becomes more expensive.
After this episode, you can build a structure with clear boundaries that is easy to navigate and does not leave new developers lost.
Instead of piling all pages into one long routes folder, group them by feature. SvelteKit supports route groups with folders prefixed by parentheses, which shape the layout hierarchy without adding URL segments.
src/
routes/
(publik)/
+layout.svelte
about/+page.svelte
(auth)/
+layout.svelte
login/+page.svelte
register/+page.svelte
(app)/
+layout.server.js
dashboard/+page.svelte
lib/
server/
db.js
auth.js
components/
ui/
utils/Route groups let different groups have different layouts: public pages without a sidebar, application pages with a sidebar and an auth guard. These boundaries keep layout responsibilities close to the features that use them, instead of in one global layout.
Data functions and business logic should not live inside components. Move them into services in src/lib/server: load functions and server actions call the services, and components only receive data and render it.
// src/lib/server/layanan/pengguna.js
import db from "$lib/server/db";
export const daftarPengguna = async ({ halaman, limit }) => {
return db.query(
"SELECT id, nama, email FROM pengguna ORDER BY id DESC LIMIT $2 OFFSET $1",
[halaman, limit]
);
};A thin load function is easy to test and read: it takes parameters, calls a service, and returns data. Complex logic moves into services that can be unit-tested independently, and authorization policy stays in one place.
import { daftarPengguna } from "$lib/server/layanan/pengguna";
export const load = async ({ locals, url }) => {
const halaman = Number(url.searchParams.get("halaman") ?? 1);
return {
pengguna: await daftarPengguna({ halaman, limit: 20 })
};
};The {@render} snippet lets you define a piece of UI once and render it many times with different data. This reduces markup duplication without needing a full component.
{#snippet kartu(item)}
<article class="kartu">
<h3>{item.judul}</h3>
<p>{item.ringkasan}</p>
</article>
{/snippet}
{#each items as item}
{@render kartu(item)}
{/each}Pure functions like date formatting, validation, or slugify belong in src/lib/utils with descriptive names. Pure functions without side effects are the easiest to test and reuse in many places, including on the server.
Establish naming conventions for route files, components, and services, then write them down in the README or contributing guide. Also document important architecture decisions: when to use +server.js, when to use server actions, and where authorization is enforced.
Run npm run check and npm run lint regularly in CI to enforce consistency. Strict types on load functions make the contract between server and client explicit, so a change on one side does not silently break the other.
Key takeaways:
src/lib/utils are easy to test and reuse.In the next episode we get into modern tooling & build automation: Vite integration and preprocessors, TypeScript setup and strict typing, CI/CD pipelines for SvelteKit apps, plus linting, formatting, and pre-commit hooks.