The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
Sapper’s succesor, SvelteKit, is currently available for use. All development efforts moving forward will be focused on SvelteKit.For Svelte developers, this means, in most cases, you should consider migrating from Sapper to SvelteKit. The Sapper docs include a helpful migration guide for Svelte developers looking to make the switch. That said, you may still want to use Sapper if you’re squeamish about the potential roadblocks associated with using beta software, or if you want direct access to Express/Polka. If you still want to use Sapper despite its lack of support, read on to learn how.
In war, the soldiers who build bridges, repair roads, clear minefields and conduct demolitions — all under combat conditions — are known as sappers.For web developers, the stakes are generally lower than for combat engineers. But we face our own hostile environment: underpowered devices, poor network connections, and the complexity inherent in front-end engineering. Sapper, which is short for Svelte app maker, is your courageous and dutiful ally. Hmm, makes perfect sense 🤓. Sapper (and, by extension, Svelte) is designed to be lightweight, performant, and easy to reason about while still providing you with enough features to turn your ideas into awesome web apps. Basically, here are the things Sapper helps take care of for you when building web apps in Svelte:
$ npx degit "sveltejs/sapper-template#rollup" my-app $ cd my-app $ npm install $ npm run devDoing that will get you a bare-bones project, but that will be enough for the purpose of this article. We should be able to explore how Sapper handles routing and server-side rendering with this simple project without going too deep. Let’s dive in!
src/template.html file:
src/client.jssrc/server.jssrc/service-worker.js (this one is optional)client.jsimport * as sapper from '@sapper/app';
sapper.start({
target: document.querySelector('#sapper')
});
This is the entry point of the client-rendered app. It is a fairly simple file, and all you need to do here is import the main Sapper module from @sapper/app and call the start method from it. This takes in an object as an argument, and the only required key is the target.
The target specifies which DOM node the app is going to be mounted on. If you’re coming from a React.js background, think of this as ReactDOM.render.
server.js/static folder. Sapper doesn’t care how you do it. Just serve that folder!sapper.middleware() imported from @sapper/server.process.env.PORT.src/server.js file generated for us to see what it looks like in practice.
service-worker.jsservice-worker.js file is not required for you to build a fully functional web app with Sapper; it only gives you access to features like offline support, push notifications, background synchronization, etc.
Since Service Workers are custom to apps, there are no hard-and-fast rules for how to write one. You can choose to leave it out entirely, or you could utilize it to provide a more complete user experience.
template.htmlrouteshttp://localhost:3000 should take you to a simple web app with a homepage, an about page, and a blog page. So far, so simple.
Now let’s try to understand how Sapper is able to reconcile the URL with the corresponding file. In Sapper, there are two types of routes: page routes and server routes.
Let’s break it down further.
/about — Sapper renders an about.svelte file located in the src/routes folder. This means that any .svelte file inside of that folder is automatically “mapped” to a route of the same name. So, if you have a file called jumping.svelte inside the src/routes folder, navigating to /jumping will result in that file being rendered.
In short, page routes are .svelte files under the src/routes folder. A very nice side effect of this approach is that your routes are predictable and easy to reason about. You want a new route? Create a new .svelte file inside of src/routes and you’re golden!
What if you want a nested route that looks like this: /projects/sapper/awesome? All you need to do is create a folder for each subroute. So, for the above example, you will have a folder structure like this: src/routes/projects/sapper, and then you can place an awesome.svelte file inside of the /sapper folder.
With this in mind, let’s take a look at our bootstrapped app and navigate to the “about” page. Where do you think the content of this page is being rendered from? Well, let’s take a look at src/routes. Sure enough, we find an about.svelte file there — simple and predictable!
Note that the index.svelte file is a reserved file that is rendered when you navigate to a subroute. For example, in our case, we have a /blogs route where we can access other subroutes under it, e.g., /blogs/why-the-name.
But notice that navigating to /blogs in a browser renders a file when /blogs is a folder itself. How do you choose what file to render for such a route?
Either we define a blog.svelte file outside the /blogs folder, or we would need an index.svelte file placed under the /blogs folder, but not both at the same time. This index.svelte file gets rendered when you visit /blogs directly.
What about URLs with dynamic slugs? In our example, it wouldn’t be feasible to manually create every single blog post and store them as .svelte files. What we need is a template that is used to render all blog posts regardless of the slug.
Take a look at our project again. Under src/routes/blogs, there’s a [slug].svelte file. What do you think that is? Yup — it’s the template for rendering all blog posts regardless of the slug. This means that any slug that comes after /blogs is automatically handled by this file, and we can do things like fetching the content of the page on page mount and then rendering it to the browser.
Does this mean that any file or folder under /routes is automatically mapped to a URL? Yes, but there’s an exception to this rule. If you prefix a file or folder with an underscore, Sapper doesn’t convert it to a URL. This makes it easy for you to have helper files inside the routes folder.
Say we wanted a helpers folder to house all our helper functions. We could have a folder like /routes/_helpers, and then any file placed under /_helpers would not be treated as a route. Pretty nifty, right?
[slug].svelte file that would help us match any URL like this: /blogs/<any_url>. But how does it actually get the content of the page to render?
You could get the content from a static file or make an API call to retrieve the data. Either way, you would need to make a request to a route (or endpoint, if you think only in API) to retrieve the data. This is where server routes come in.
From the official docs: “Server routes are modules written in .js files that export functions corresponding to HTTP methods.”
This just means that server routes are endpoints you can call to perform specific actions, such as saving data, fetching data, deleting data, etc. It’s basically the backend for your app so you have everything you need in one project (you could split it if you wanted, of course).
Now back to our bootstrapped project. How do you fetch the content of every blog post inside [slug].svelte? Well, open the file, and the first bit of code you see looks like this:
<script context="module">
export async function preload({ params, query }) {
// the `slug` parameter is available because
// this file is called [slug].html
const res = await this.fetch(`blog/${params.slug}.json`);
const data = await res.json();
if (res.status === 200) {
return { post: data };
} else {
this.error(res.status, data.message);
}
}
</script>
All we are looking at is a simple JS function that makes a GET request and returns the data from that request. It takes in an object as a parameter, which is then destructured on line 2 to get two variables: params and query.
What do params and query contain? Why not add a console.log() at the beginning of the function and then open a blog post in the browser? Do that and you get something like this logged to the console:
{slug: "why-the-name"}slug: "why-the-name"__proto__: Object {}
Hmm. So if we opened the “why-the-name” post on line 5, our GET request would be to blog/why-the-name.json, which we then convert to a JSON object on line 6.
On line 7, we check if our request was successful and, if yes, return it on line 8 or else call a special method called this.error with the response status and the error message.
Pretty simple. But where is the actual server route, and what does it look like? Look inside src/routes/blogs and you should see a [slug].json.js file — that is our server route. And notice how it is named the same way as [slug].svelte? This is how Sapper maps a server route to a page route. So if you call this.fetch inside a file named example.svelte, Sapper will look for an example.json.js file to handle the request.
Now let’s decode [slug].json.js, shall we?
import posts from './_posts.js';
const lookup = new Map();
posts.forEach(post => {
lookup.set(post.slug, JSON.stringify(post));
});
export function get(req, res, next) {
// the `slug` parameter is available because
// this file is called [slug].json.js
const { slug } = req.params;
if (lookup.has(slug)) {
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(lookup.get(slug));
} else {
res.writeHead(404, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
message: `Not found`
}));
}
}
What we’re really interested in here begins from line 8. Lines 3–6 are just preparing the data for the route to work with. Remember how we made a GET request in our page route: [slug].svelte? Well, this is the server route that handles that request.
If you’re familiar with Express.js APIs, then this should look familiar to you. That is because this is just a simple controller for an endpoint. All it is doing is taking the slug passed to it from the Request object, searching for it in our data store (in this case, lookup), and returning it in the Response object.
If we were working with a database, line 12 might look something like Posts.find({ where: { slug } }) (Sequelize, anyone?). You get the idea.
Server routes are files containing endpoints that we can call from our page routes. So let’s do a quick rundown of what we know so far:
.svelte files under the src/routes folder that render content to the browser..js files that contain API endpoints and are mapped to specific page routes by name.window object, and as you know, you can’t access window from the server side. Simply importing such a module will cause your compile to fail, and the world will become a bit dimmer 🥺.
Not to fret, though; there is a simple fix for this. Sapper allows you to import modules dynamically (hey, smaller initial bundle sizes) so you don’t have to import the module at the top level. What you do instead will look something like this:
<script>
import { onMount } from 'svelte';
let MyComponent;
onMount(async () => {
const module = await import('my-non-ssr-component');
MyComponent = module.default;
});
</script>
<svelte:component this={MyComponent} foo="bar"/>
On line 2, we’re importing the onMount function. The onMount function comes built into Svelte, and it is only called when the component is mounted on the client side (think of it like the equivalent of React’s componentDidMount).
This means that when only importing our problematic module inside the onMount function, the module is never called on the server, and we don’t have the problem of a missing window object. There! Your code compiles successfully and all is well with the world again.
Oh, and there’s another benefit to this approach: since you’re using a dynamic import for this component, you’re practically shipping less code initially to the client side.

Rosario De Chiara discusses why small language models (SLMs) may outperform giants in specific real-world AI systems.

Vibe coding isn’t just AI-assisted chaos. Here’s how to avoid insecure, unreadable code and turn your “vibes” into real developer productivity.

GitHub SpecKit brings structure to AI-assisted coding with a spec-driven workflow. Learn how to build a consistent, React-based project guided by clear specs and plans.

:has(), with examplesThe CSS :has() pseudo-class is a powerful new feature that lets you style parents, siblings, and more – writing cleaner, more dynamic CSS with less JavaScript.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now
7 Replies to "Sapper and Svelte: A quick tutorial"
Hey thanks for this thorough and well written article. I’ve made a few videos about Sapper tutorials if anyone is interested: https://www.youtube.com/watch?v=kGfplN8HtlQ
Hi, thanks for this article. Is Sapper ready for production applications? As of this writing, Sapper is still on v0.27.8
Hey btw there aren’t any line numbers on the code samples
Hi I have read a couple of your articles which have been very useful and highly informative so thanks!
Hey Paul, thanks for your nice comment!
Have a nice day.
nice Article, but I don’t know if its just the blog demo, but I find the whole concept of server routing with slugs to be a confusing mess in Sapper, I’m sure I’m not alone.. other routers make routing so much easier… maybe its just the way the blog was setup to work with local json data… I am alone on feeling this way?
Actually I take it back, after playing with Sapper for a hour or so, slug routes are not complicated (or messy) Once you strip it back to its bare bones ie.. just routing… its pretty straight forward and a joy to use, I do think the /blog tutorial is a little overkill for a template, and this would have been better for a follow up tutorial.. anyway.. anyone who is wanting some extra help in making routing with slugs easier to understand before jumping into endpoints and server routing, Ive made a cloned version of the starter template with a profile route, which has a route for the main profile route (index.svelte) and a slugged version for the individual profile page for the user.. Once you see how easy slug routes can be when everything else is stripped away.. I think the /blog example becomes much easier to understand… anyone interested can clone my repo at:-
https://github.com/PBLinuxLT/svelte_sapper_easy_routing_examples
Thanks & Happy Svelting..