Foundation
Welcome to the SvelteKit Routing series—a comprehensive, documentation-style guide that goes beyond the basics. Whether you’re coming from React Router, Vue Router, or building your first modern web application, this series will give you the deep understanding that separates junior developers from senior engineers.
Throughout this series, you’ll learn about routes, pages, layouts, data loading, error handling, and more. Each article dives into a specific file or concept in SvelteKit’s routing system, explaining not just the “how” but the crucial “why” behind each design decision.
You’ll explore:
- Dynamic Pages — Build pages that respond to URL parameters
- Server Pages — Load data securely on the server
- API Endpoints — Build a full backend API within your SvelteKit app
- Layouts — Share UI and data across multiple routes
By the end of this article, you’ll understand the philosophical “why” behind SvelteKit’s routing choices and have a clear mental model of how everything fits together.
Why Understanding Philosophy MattersUnderstanding why a framework makes certain choices is more valuable than memorizing how to use it. When you grasp the underlying philosophy, you can predict behavior, debug faster, and make better architectural decisions.
What is Filesystem-Based Routing?
Before we write any code, let’s understand why SvelteKit chose filesystem-based routing. This isn’t just a technical detail—it’s a philosophy that will shape how you think about your entire application.
The Traditional Approach (And Its Problems)
In many frameworks, you define routes in a configuration file:
// Traditional routing configuration (NOT SvelteKit)
const routes = [
{ path: '/', component: HomePage },
{ path: '/about', component: AboutPage },
{ path: '/blog', component: BlogPage },
{ path: '/blog/:slug', component: BlogPostPage },
{ path: '/contact', component: ContactPage }
] This approach has a fundamental problem: separation of concerns becomes separation of understanding. Your route definitions live in one place, your components live elsewhere, and your data-loading logic might be scattered across multiple files. When you need to modify the /about page, you must:
- Find the route configuration to understand what component renders
- Navigate to that component file
- Hunt for any associated data-loading logic
- Hope you didn’t miss any related server-side code
SvelteKit’s Philosophy: Co-location
SvelteKit eliminates this cognitive overhead through co-location—the principle that related code should live together.
In SvelteKit, the folder structure IS the router. If you want a route at /about, you simply create a folder named about. You don’t need to register this folder anywhere; its mere existence creates the route.
When you place a +page.svelte file inside that folder (src/routes/about/+page.svelte), you have simultaneously:
- Declared a route that responds to
/about(because of the folder name). - Created the component that renders for that route (the
+page.sveltefile). - Established the location for any data-loading logic (
+page.jsgoes right next to it). - Defined where server-side code lives (
+page.server.jsis right there too).
This isn’t just convenient—it’s transformative. Instead of jumping between a router config file, a component folder, and an API folder, everything related to the “About” page lives in one place. Need to change the about page? Go to src/routes/about/. Everything you need is there. No hunting, no guessing.
The src/routes Directory: Your Application’s Map
Every SvelteKit application has a src/routes directory. This directory IS your routing configuration. Let’s see how directories map to URLs:
src/routes/ → /
src/routes/about/ → /about
src/routes/blog/ → /blog
src/routes/contact/ → /contact
src/routes/products/ → /products
src/routes/products/shoes/ → /products/shoes The pattern is beautifully simple: each directory becomes a URL segment.
Dynamic Routes
But what if you have hundreds of blog posts? You don’t want to create a folder for each one. SvelteKit uses square brackets to create dynamic route parameters:
src/routes/blog/[slug]/+page.svelte → /blog/hello-world
→ /blog/my-first-post
→ /blog/anything-here The [slug] folder matches any value, and that value becomes available in your load functions and components.
Understanding Dynamic Route Patterns
SvelteKit supports several dynamic route patterns, each designed for specific use cases:
| Pattern | Example | Matches |
|---|---|---|
[slug] | /blog/[slug] | /blog/hello (required) |
[[lang]] | /[[lang]]/about | /about or /en/about (optional) |
[...path] | /files/[...path] | /files/a/b/c (rest/catch-all) |
[id=integer] | /posts/[id=integer] | /posts/123 (with validation) |
Required parameters [slug] are the most common—they match any single segment. A route like /blog/[slug] will match /blog/hello but not /blog or /blog/hello/world.
Optional parameters [[lang]] let a single route handle multiple URL patterns. For example, [[lang]]/about matches both /about and /en/about. This is perfect for internationalization where you want a default language at the root and localized versions under language codes. We’ll explore optional parameters in depth in a dedicated article later in this series.
Rest parameters [...path] capture everything after a certain point in the URL. A route like /files/[...path] matches /files/a, /files/a/b, and /files/a/b/c/d. The captured value becomes "a", "a/b", or "a/b/c/d" respectively. These are essential for building file browsers, documentation sites, or any feature where the URL depth is unknown. Rest parameters also enable custom 404 handling within specific sections of your app—we’ll cover this pattern in our Advanced Routing article.
Matchers [id=integer] add validation to dynamic segments. Instead of accepting any value, they only match when the parameter passes a test you define. For instance, [id=integer] only matches numeric IDs like /posts/123, not /posts/hello. Matchers are defined in your src/params directory and help create type-safe routes. We’ll explore how to create custom matchers when we cover Advanced Routing patterns.
Each of these patterns serves a specific purpose in building robust routing logic. As you progress through this series, you’ll see practical examples of when and why to use each one.
Route Matching Priority
When multiple routes could potentially match a URL, SvelteKit needs to decide which one to use. Understanding this priority system prevents confusing bugs and helps you structure routes effectively.
The golden rule: more specific routes win. SvelteKit sorts routes from most specific to least specific, then tries them in order until one matches. Here’s what “specific” means:
- Static routes beat dynamic routes.
/blog/aboutwins over/blog/[slug] - Routes with matchers beat routes without.
/posts/[id=integer]wins over/posts/[slug] - Required parameters beat optional and rest parameters.
/[slug]wins over/[[optional]]or/[...rest]
Consider these routes:
src/routes/blog/latest/+page.svelte ← Highest priority (static)
src/routes/blog/[slug=integer]/+page.svelte ← Second (has matcher)
src/routes/blog/[slug]/+page.svelte ← Third (dynamic, no matcher)
src/routes/blog/[[id]]/+page.svelte ← Fourth (optional)
src/routes/[...catchall]/+page.svelte ← Lowest priority (rest) When a user visits /blog/latest, SvelteKit tries routes in order and stops at the first match—the static route. When they visit /blog/123, it skips the static route (doesn’t match “latest”), then matches the integer-validated route. For /blog/hello-world, it skips down to the plain [slug] route.
This sorting happens automatically. You don’t configure it, but you do need to understand it when designing your route structure. If you’re getting unexpected matches, check whether a more general route is catching requests before they reach your intended route. We’ll explore route matching edge cases and debugging strategies in our Advanced Routing article.
The Special + Prefix
Within any route directory, SvelteKit recognizes special files by their + prefix. The + wasn’t chosen randomly — it’s a character that sorts to the top of directory listings in most file explorers, keeping route files visually grouped and separate from non-route files. More importantly, it creates an unambiguous convention: anything beginning with + is owned by the SvelteKit router, anything without it is yours to organize freely.
This design solves a real problem: in a filesystem-based router, you might want to co-locate utilities, components, or test files next to your routes. Without a clear convention, the router would have to either forbid that (forcing you to separate them) or use heuristics to guess which files are routes. The + prefix makes the distinction explicit and consistent.
Here is a summary of the files you’ll encounter. Each has a dedicated article in this series:
| File | Purpose | Execution Environment | Article |
|---|---|---|---|
+page.svelte | Renders the page UI | Server (SSR) + Client | +page.svelte |
+page.js | Universal data loading | Server + Client | +page.js |
+page.server.js | Server-only data loading | Server only | +page.server.js |
+layout.svelte | Shared layout wrapper | Server (SSR) + Client | +layout.svelte |
+layout.js | Universal layout data loading | Server + Client | +layout.js |
+layout.server.js | Server-only layout data loading | Server only | +layout.server.js |
+error.svelte | Error boundary UI | Server (SSR) + Client | +error.svelte |
+server.js | API endpoint handlers | Server only | +server.js |
You’ll notice two data-loading files for each level (+page.js and +page.server.js, +layout.js and +layout.server.js). This is one of SvelteKit’s most important architectural decisions. The .server variants run exclusively on the server and have access to databases, private API keys, and cookies — none of their code ever reaches the browser. The non-.server variants run on both server and browser, which enables instant client-side navigation (no server round-trip on page changes) but means they can only use code that works in both environments. The choice between them comes down to: does this data require server-only capabilities? If yes, use .server. If the data is public and safe to compute in the browser, use the universal version.
Files without the + prefix are ignored by SvelteKit’s router — you can safely co-locate helper functions, components, or utilities right next to your routes.
For example, this structure is perfectly valid:
src/routes/blog/
├── +page.svelte # Route file (SvelteKit recognizes this)
├── +page.js # Route file (SvelteKit recognizes this)
├── BlogCard.svelte # Component (ignored by router)
├── utils.js # Helper functions (ignored by router)
└── formatDate.js # Utility (ignored by router) SvelteKit only cares about the + prefixed files. Everything else is yours to organize however makes sense for your project. This co-location of related code is one of SvelteKit’s most powerful features—your components, utilities, and route logic can live together, making refactoring and maintenance much easier.
A Typical Route Directory
To see how these files work together, let’s look at a typical route structure:
src/routes/blog/
├── +page.svelte # The UI users see
├── +page.ts # Loads data for the page
├── +layout.svelte # Wraps this and child pages
├── helpers.ts # Ignored by router (no + prefix)
└── [slug]/
├── +page.svelte # Individual blog post UI
└── +page.server.ts # Server-only data loading When a user visits /blog:
- SvelteKit finds
src/routes/blog/ - Checks if
+page.jsexists to load data for the page - Renders
+page.svelteinside+layout.svelte
This is a simplified example, but the pattern is predictable and consistent. Everything is co-located.
Understanding the Root Layout
The root layout at src/routes/+layout.svelte deserves special attention because it wraps your entire application. Every page, every nested layout, every route renders inside this root layout.
If you don’t create a root layout, SvelteKit provides a minimal default:
<script>
let { children } = $props()
</script>
{@render children()} This default just renders your pages without any wrapper. But in most real applications, you’ll want to add your own root layout to include site-wide elements like navigation, footers, analytics, or global styles.
The root layout is also special in terms of error handling. If an error occurs in the root layout’s own load function — not in a page, but in the layout itself — SvelteKit can’t render the root layout to show the error, because the root layout is what renders error pages. In this case, SvelteKit falls back to a static src/error.html file. This is the only situation where SvelteKit bypasses the entire component hierarchy and renders a raw HTML file. Understanding this edge case matters for production: keep your root layout’s load function minimal and robust, because a failure there produces the most disruptive possible user experience.
We’ll explore layout hierarchies and error handling boundaries in detail in the Layouts section of this series.
How Routes Resolve: The Complete Picture
Understanding how SvelteKit resolves a URL into a rendered page helps you predict behavior and debug issues. Let’s walk through what happens when a user visits /blog/hello-world:
Step 1: Route Matching
SvelteKit examines your src/routes directory and finds the best match:
src/routes/blog/[slug]/+page.svelte ← Matches! The [slug] parameter captures "hello-world" and makes it available as params.slug.
Step 2: Loading Data (Top to Bottom)
Before rendering anything, SvelteKit runs all load functions in the layout hierarchy, starting from the root:
1. src/routes/+layout.server.js (if it exists)
2. src/routes/blog/+layout.server.js (if it exists)
3. src/routes/blog/[slug]/+page.server.js (if it exists) Each load function receives data from parent load functions via parent(). This allows child routes to build on data loaded higher up. Server load functions run only on the server and have access to databases, environment variables, and other server-only resources.
Step 3: Rendering the Hierarchy (Outside to Inside)
Once all data is loaded, SvelteKit renders the layout hierarchy from the outermost layout to the innermost page:
src/routes/+layout.svelte
└── wraps src/routes/blog/+layout.svelte
└── wraps src/routes/blog/[slug]/+page.svelte Each layout receives data from its corresponding load function and children containing the next level down. The innermost component is your +page.svelte, which renders the actual page content.
Step 4: Hydration (Server to Client)
On the initial page load, this entire hierarchy is rendered on the server (SSR) and sent as HTML. When the JavaScript loads, Svelte “hydrates” the page—attaching event listeners and making it interactive. On subsequent navigation, SvelteKit only runs client-side load functions and swaps out the page without a full page reload.
This resolution process happens for every route in your application. Understanding it helps you reason about data flow, decide where to place load functions, and structure your layouts effectively. We’ll dive deeper into data loading strategies, layout composition, and client-side navigation in upcoming articles.
The Mental Model
Think of your src/routes directory as a tree:
src/routes/
├── +layout.svelte (root - wraps everything)
├── +page.svelte (home page)
├── about/
│ └── +page.svelte
└── blog/
├── +layout.svelte (wraps blog pages)
├── +page.svelte (blog index)
└── [slug]/
└── +page.svelte (individual posts) Conclusion
SvelteKit’s routing system represents a fundamental shift in how we think about building web applications. By making the filesystem the source of truth for your application’s URL structure, it eliminates the disconnect between routes and code that plagues traditional router configurations. Every folder becomes a URL segment, every +page.svelte becomes a route, and the relationships between layouts, pages, and data files emerge naturally from the directory structure itself.
This filesystem-based approach isn’t just about convenience—it’s about creating applications that are easier to understand, easier to maintain, and easier to scale. When routes are defined by file location rather than configuration, new developers can navigate your codebase intuitively, refactoring becomes a matter of moving files, and the relationship between URL structure and code organization remains clear even as applications grow. By mastering these foundational concepts, you build the mental model needed to leverage SvelteKit’s full power.
Key Takeaways
- Filesystem-based routing uses folder structure to define URL segments -
src/routes/blog/[slug]creates the/blog/:slugroute pattern automatically +page.sveltedefines renderable routes - folders without this file serve as URL segments but don’t render pages themselves+layout.sveltewraps descendant routes creating shared UI that persists across navigation, with nested layouts forming a component hierarchy- Data files provide server/client data -
+page.js(universal),+page.server.js(server-only), and corresponding layout files load data for their components - Dynamic segments use brackets -
[param]captures URL segments as parameters,[...rest]matches remaining paths, and[[optional]]makes segments optional - The root layout wraps everything -
src/routes/+layout.svelteprovides the HTML shell and global UI for your entire application - Convention over configuration philosophy eliminates route configuration files and makes the relationship between URLs and code explicit through file structure
- Co-located route files enable modularity - pages, layouts, data loading, and API endpoints live together, making features self-contained and easy to refactor
See Also
- Official SvelteKit Documentation - Routing - The official guide to SvelteKit’s routing system and file conventions
- SvelteKit Project Structure - Understanding the src/routes directory
- Dynamic Parameters - Param matchers and advanced patterns
- File-system Routing Comparison - How other frameworks approach filesystem routing