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.js
src/server.js
src/service-worker.js
(this one is optional)client.js
import * 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.js
service-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.html
routes
http://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.
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 nowDiscover Float UI, a set of pre-made templates that leverage the power of Tailwind CSS to help developers create professional websites quickly.
Learn how to use React-Toastify in 2025, from setup to styling and advanced use cases like API notifications, async toasts, and React-Toastify 11 updates.
Discover open source tools for cross-browser CSS testing like Playwright and BrowserStack to catch rendering errors, inconsistent styling, and more.
With the introduction of React Suspense, handling asynchronous operations like data fetching has become more efficient and declarative.
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..