12 min read
0%

SvelteKit Best Practices

Back to Blog
SvelteKit Best Practices

SvelteKit Best Practices

SvelteKit has revolutionized the way we build web applications. This comprehensive guide covers best practices for routing, data loading, error handling, and more to help you build robust, scalable applications.

Project Structure

A well-organized project structure is crucial:

src/
├── lib/
│   ├── components/
│   ├── stores/
│   ├── utils/
│   └── types/
├── routes/
│   ├── api/
│   ├── (app)/
│   └── (marketing)/
└── app.html

Routing Best Practices

Route Groups

Organize routes without affecting URLs:

routes/
├── (app)/
│   ├── dashboard/
│   └── settings/
└── (marketing)/
    ├── about/
    └── pricing/

Advanced Layouts

Use layout inheritance effectively:

<!-- src/routes/(app)/+layout.svelte -->
<script>
  import { page } from '$app/stores';
  export let data;
</script>

<nav>
  {#each data.navItems as item}
    <a href={item.href} aria-current={$page.url.pathname === item.href ? 'page' : undefined}>
      {item.label}
    </a>
  {/each}
</nav>

<slot />

Data Loading

Server Load Functions

Load data efficiently on the server:

// src/routes/blog/+page.server.ts
import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ fetch, params }) => {
  const response = await fetch("/api/posts");
  const posts = await response.json();

  return {
    posts,
    meta: {
      title: "Blog Posts",
      description: "Latest articles and tutorials",
    },
  };
};

Universal Load Functions

Share code between server and client:

// src/routes/posts/[slug]/+page.ts
import type { PageLoad } from "./$types";

export const load: PageLoad = async ({ fetch, params }) => {
  const post = await fetch(`/api/posts/${params.slug}`).then((r) => r.json());

  return {
    post,
    streamed: {
      comments: fetch(`/api/posts/${params.slug}/comments`).then((r) =>
        r.json(),
      ),
    },
  };
};

Form Actions

Progressive Enhancement

Build forms that work without JavaScript:

<!-- src/routes/contact/+page.svelte -->
<script>
  import { enhance } from '$app/forms';
  export let form;
</script>

<form method="POST" use:enhance>
  <input name="email" type="email" required />
  <textarea name="message" required></textarea>

  {#if form?.success}
    <p class="success">Message sent!</p>
  {/if}

  {#if form?.errors}
    <p class="error">{form.errors.message}</p>
  {/if}

  <button type="submit">Send</button>
</form>
// src/routes/contact/+page.server.ts
import type { Actions } from "./$types";
import { fail } from "@sveltejs/kit";

export const actions = {
  default: async ({ request }) => {
    const data = await request.formData();
    const email = data.get("email");
    const message = data.get("message");

    if (!email || !message) {
      return fail(400, { errors: { message: "All fields required" } });
    }

    await sendEmail(email, message);

    return { success: true };
  },
} satisfies Actions;

Error Handling

Error Pages

Create custom error pages:

<!-- src/routes/+error.svelte -->
<script>
  import { page } from '$app/stores';
</script>

<h1>{$page.status}: {$page.error?.message}</h1>

{#if $page.status === 404}
  <p>This page doesn't exist!</p>
{:else}
  <p>Something went wrong.</p>
{/if}

Expected Errors

Handle expected errors gracefully:

import { error } from "@sveltejs/kit";

export const load: PageServerLoad = async ({ params }) => {
  const post = await db.post.findUnique({
    where: { slug: params.slug },
  });

  if (!post) {
    throw error(404, {
      message: "Post not found",
    });
  }

  return { post };
};

API Routes

RESTful Endpoints

Build type-safe API routes:

// src/routes/api/posts/+server.ts
import type { RequestHandler } from "./$types";
import { json } from "@sveltejs/kit";

export const GET: RequestHandler = async ({ url }) => {
  const limit = Number(url.searchParams.get("limit") ?? 10);
  const posts = await db.post.findMany({ take: limit });

  return json(posts);
};

export const POST: RequestHandler = async ({ request }) => {
  const data = await request.json();
  const post = await db.post.create({ data });

  return json(post, { status: 201 });
};

Performance Optimization

Preload Data

Preload critical data:

<script>
  import { preloadData } from '$app/navigation';

  function preload(href) {
    preloadData(href);
  }
</script>

<a href="/posts" on:mouseenter={() => preload('/posts')}>
  View Posts
</a>

Lazy Loading

Load components on demand:

<script>
  let HeavyComponent;

  async function loadComponent() {
    const module = await import('$lib/components/HeavyComponent.svelte');
    HeavyComponent = module.default;
  }
</script>

<button on:click={loadComponent}>Load Component</button>

{#if HeavyComponent}
  <svelte:component this={HeavyComponent} />
{/if}

State Management

Stores

Use Svelte stores for shared state:

// src/lib/stores/user.ts
import { writable } from "svelte/store";

function createUserStore() {
  const { subscribe, set, update } = writable(null);

  return {
    subscribe,
    login: (userData) => set(userData),
    logout: () => set(null),
    updateProfile: (updates) => update((user) => ({ ...user, ...updates })),
  };
}

export const user = createUserStore();

Context API

Share data within component trees:

<!-- Parent.svelte -->
<script>
  import { setContext } from 'svelte';

  setContext('theme', {
    primary: '#007bff',
    secondary: '#6c757d'
  });
</script>

<!-- Child.svelte -->
<script>
  import { getContext } from 'svelte';

  const theme = getContext('theme');
</script>

<style>
  button {
    background: v-bind('theme.primary');
  }
</style>

Security

CSRF Protection

SvelteKit includes built-in CSRF protection for form actions. Always use it:

// Automatically enabled for form actions
export const actions = {
  default: async ({ request }) => {
    // CSRF token is automatically validated
    const data = await request.formData();
    // Process form...
  },
};

Content Security Policy

Configure CSP headers:

// src/hooks.server.ts
export const handle = async ({ event, resolve }) => {
  const response = await resolve(event);

  response.headers.set(
    "Content-Security-Policy",
    "default-src 'self'; script-src 'self' 'unsafe-inline'",
  );

  return response;
};

Testing

Unit Tests

Test your components:

import { render } from "@testing-library/svelte";
import Button from "./Button.svelte";

test("renders button with text", () => {
  const { getByText } = render(Button, { props: { label: "Click me" } });
  expect(getByText("Click me")).toBeInTheDocument();
});

Integration Tests

Test your routes:

import { expect, test } from "@playwright/test";

test("homepage loads", async ({ page }) => {
  await page.goto("/");
  await expect(page.locator("h1")).toContainText("Welcome");
});

Deployment

Adapters

Choose the right adapter for your platform:

// svelte.config.js
import adapter from "@sveltejs/adapter-auto"; // Auto-detects platform

export default {
  kit: {
    adapter: adapter(),
  },
};

Environment Variables

Use environment variables securely:

// Access server-side only
import { env } from "$env/dynamic/private";
const apiKey = env.API_KEY;

// Access client-side (prefixed with PUBLIC_)
import { env } from "$env/dynamic/public";
const publicApiUrl = env.PUBLIC_API_URL;

Conclusion

SvelteKit provides powerful features for building modern web applications. By following these best practices, you can create apps that are fast, maintainable, and delightful to work with.

Remember: Convention over configuration, but flexibility when you need it.


Browser support snapshot

Live support matrix for es6-module-dynamic-import from Can I Use.

Show static fallback image Data on support for es6-module-dynamic-import across major browsers from caniuse.com

Source: caniuse.com

Canvas is not supported in your browser