DEV Community

jamilxt
jamilxt

Posted on

SvelteKit 3 Just Shipped. Here Is Your Migration Checklist.

Yesterday the Svelte team shipped SvelteKit 3.0, and by this morning the announcement was sitting on the Hacker News front page with over 350 points and more than 150 comments. That is a strong signal. When a framework whose previous major version came out in 2022 jumps a whole number, thousands of production apps have a migration in their future, and the comment section fills up with people comparing notes on what broke.

I write TypeScript and Svelte as part of my daily toolkit, so this release matters to me directly. Full disclosure though: I have not yet run this migration on a production app, so this is not a war story. It is the opposite. I read the release announcement, the release candidate post, and the full official migration guide end to end, and turned them into the checklist I will follow when I upgrade. Every claim below links to the official source, so you can verify anything yourself.

Here is everything that changes, in roughly the order you will hit it.

Step 0: Check your floor versions first

Before touching any code, SvelteKit 3 raises the minimum versions of everything underneath it. From the migration guide, you need at minimum:

  • Node v22.17 or newer
  • TypeScript v6 or newer
  • Svelte v5.56.4 or newer
  • Vite v8.0.12, the first Vite 8 release bundling stable Rolldown v1
  • @sveltejs/vite-plugin-svelte v7

Two of these deserve attention. The TypeScript 6 requirement will catch teams that pinned TS 5 for plugin compatibility. And SvelteKit 3 requires Svelte 5, which is the entire reason the error handling story improves later in this list.

The guide also recommends upgrading to the most recent 2.x release before jumping to 3.0, because 2.x emits targeted deprecation warnings that map one to one onto the breaking changes. Skipping that step means losing your best migration assistant.

The config file is dead. Long live vite.config.ts

The biggest structural change: svelte.config.js is no longer supported at all. All configuration now lives in the Vite plugin options inside vite.config.ts.

The release candidate post explains the reasoning, and it is sound. The Vite plugin previously had to resolve your config through an asynchronous process that could not start until the entire Vite config was resolved, because tools like Vitest can run with a working directory that is not the project root. Moving config into the plugin call gives the plugin immediate access. And honestly, the simpler question: why were we maintaining two config files for one project?

In practice, config.kit.* options become top-level plugin options:

import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import adapter from '@sveltejs/adapter-auto';

export default defineConfig({
    plugins: [
        sveltekit({
            compilerOptions: { experimental: { async: true } },
            adapter: adapter()
        })
    ]
});
Enter fullscreen mode Exit fullscreen mode

A few options died entirely and should just be deleted, per the migration guide:

  • preloadStrategy is removed. modulepreload is now supported everywhere, so SvelteKit always uses it.
  • prerender.origin is replaced by a top-level paths.origin option.
  • csrf.checkOrigin is replaced by csrf.trustedOrigins.

The $lib alias becomes #lib, with a sharp edge

This is the change that touches every file in a real codebase. SvelteKit no longer generates the $lib alias. Instead you declare a #lib alias in the imports field of your package.json, using Node's built-in subpath imports, which Vite and TypeScript resolve natively:

{
    "imports": {
        "#lib": "./src/lib/index.js",
        "#lib/*": "./src/lib/*"
    }
}
Enter fullscreen mode Exit fullscreen mode

Then replace $lib with #lib across your codebase. The migration script handles most of this, but read the gotcha before you trust it: Node and TypeScript require subpath imports to be unambiguous. Importing #lib/foo fails. You must write #lib/foo.ts or #lib/foo/index.ts, with the extension. If your imports were already fully explicit, this is painless. If you relied on extensionless resolution, expect a pass through your whole src/lib tree.

TypeScript config gets simpler

In SvelteKit 2, your tsconfig.json extended the generated ./.svelte-kit/tsconfig.json. In SvelteKit 3 you extend $app/tsconfig instead, a generated file in node_modules that now carries more recommended compiler options. The RC post notes you can likely delete most of your own compilerOptions unless you have unusual needs. You should explicitly set your include and exclude arrays, and exclude should cover your service worker.

Service workers: $service-worker is gone

If you built a PWA, this is your section. The odd $service-worker module is removed. You now import what you need from $app/env, $app/paths, and the new $app/manifest module, the same modules every other part of your app uses. You can also import self from $app/service-worker for accurate typings, provided your service worker's tsconfig extends $app/tsconfig/service-worker.

Related: SvelteKit now registers your service worker with type: 'module', since module service workers are widely supported. The RC post hints that caching-strategy helpers may arrive in a future release, which would make offline-friendly PWAs meaningfully less painful.

Environment variables become explicit and validated

SvelteKit 3 graduates the explicit environment variables feature out of the experimental flag. The model shifts from "scan and hope" to "declare and verify":

  • Declare the environment variables your app depends on in src/env.ts.
  • Mark each one as publicly available or server-only, and as resolved at build time or at boot.
  • Validate them with any Standard Schema library such as Zod or Valibot.

The payoff, per the RC announcement: type-safe, secure, validated environment variables that can be auto-imported where needed, with build-time resolution enabling dead code elimination. For anyone who has shipped a missing API key to production and found out at runtime, this is the quiet headline feature of the release.

Error handling is the upgrade most people will feel

This is where the Svelte 5 requirement pays off, and where I expect the most developer happiness per line changed:

  • Real error boundaries. SvelteKit 2 could only show your +error.svelte component for errors during load, because Svelte 4 had no error boundary concept. With Svelte 5's error boundaries, render errors are handled consistently too.
  • handleError sees everything. Errors you deliberately created with error(...) were previously ignored by your handleError hook on the assumption you had handled them. Now all errors flow through it, which means one place for logging and reporting.
  • Sourcemaps on stack traces. Production stack traces get properly sourcemapped. The RC post cautions it will take a while before every adapter displays them correctly, but the plumbing is in.

Shallow routing moves to goto

pushState and replaceState are deprecated in favor of goto with options. goto('/foo', { shallow: true }) updates the URL and page state without a real navigation, and persistState: true re-applies page.state after a reload. Shallow navigations now correctly trigger beforeNavigate and friends, and invalidateAll is deprecated in favor of refreshAll, which does not wipe your page state. Small API churn, but it touches modals, wizards, and filters in typical apps.

Vite 8 with Rolldown is mandatory

SvelteKit 2 supported Vite 8; SvelteKit 3 requires it. That means faster builds through Rolldown, the Rust-based bundler, and adoption of the Vite Environment API internally. One honest caveat from the Svelte team: they do not support FetchableDevEnvironment because it forces frameworks to absorb too much complexity, and they are exploring other ways to solve the Cloudflare Workers local-development problem. If your stack depends on that specific API, read the RC post before migrating.

Small traps worth knowing before they cost you an afternoon

From the migration guide, the changes that are easy to miss:

  • External redirects must be opted into. redirect() to an external URL now requires { external: true } or an allowlist of origins. javascript: URLs stay blocked.
  • Links to the current page refresh. Clicking a link to where you already are triggers refreshAll() instead of doing nothing.
  • data-sveltekit attributes use false. The 'off' value is removed; write data-sveltekit-preload-data="false".
  • Version polling is on by default. version.pollInterval now defaults to one hour, so apps will detect new deployments without you configuring anything.
  • Enhanced forms navigate on cross-page actions. A form with use:enhance posting to an action on a different page now navigates there, matching native behavior.

The actual migration, condensed

The official command does the mechanical work and leaves a TODO list for the rest:

npx sv migrate sveltekit-3
Enter fullscreen mode Exit fullscreen mode

And here is the full checklist in one place:

  • Upgrade to the latest 2.x and fix every deprecation warning.
  • Bump Node to 22.17+, TypeScript to 6+, Svelte to 5.56.4+, Vite to 8.0.12+, vite-plugin-svelte to 7+.
  • Move svelte.config.js contents into the sveltekit() plugin options in vite.config.ts; delete dead options like preloadStrategy.
  • Add the #lib subpath imports to package.json; replace $lib and add file extensions to #lib imports.
  • Point tsconfig.json at $app/tsconfig and set explicit include/exclude.
  • Port service workers off $service-worker onto $app/env, $app/paths, $app/manifest.
  • Declare environment variables in src/env.ts and wire up validation.
  • Replace pushState/replaceState with goto shallow options; swap invalidateAll for refreshAll.
  • Add external options to any external redirects; replace 'off' with false in data-sveltekit attributes.
  • Decide deliberately whether hourly version polling suits your app.

One more expectation-setter: remote functions, the feature the Svelte team says makes "everything else look a bit clunky," are still behind an experimental flag in 3.0. Do not plan your architecture around them yet.

Is it worth it?

For most apps, yes, and the reason is not any single feature. It is that the migration is mostly mechanical, the tooling catches mistakes with diagnostics, and you land on Rolldown builds plus a genuinely better error-handling story. The changes also read like a team clearing technical debt on purpose: fewer special cases, more platform primitives, one config file instead of two. That is the kind of major version that ages well.

I write about the tools and frameworks I actually ship with, including Svelte, TypeScript, and backend engineering, every week. If that sounds useful, subscribing is free, and it is the one reliable way to see the next piece.

Have you run the SvelteKit 3 migration yet? I am especially curious whether the #lib extension requirement bit anyone with a large codebase. Tell me in the comments.

Top comments (0)