Editor’s note: This Svelte and Sapper tutorial was last updated on 2 July 2021 to include information about Sapper’s successor, SvelteKit. For more on SvelteKit, check out “Exploring SvelteKit, the newest Svelte-based framework.”
A while back, we explored Svelte.js and saw how it helps you write truly reactive apps while shipping far less code than many other frontend frameworks out there. While you could very well build a more complex app with Svelte alone, it might get messy real quick. Enter Sapper!
In this tutorial, we’ll take a high-level look at Sapper, demonstrate how it helps you build full-fledged, lightweight apps, and break down a server-rendered app. Here’s what we’ll cover:
At the Svelte Summit in October 2020, Svelte and Sapper creator Rich Harris announced in his presentation titled “Futuristic Web Development” that he and his team were developing SvelteKit to replace Sapper.
In his follow-up blog post, Harris explained the reasoning behind the shift. For one thing, SvelteKit is designed to simplify onboarding, reduce the maintenance and support burden, and provide a predictable project structure.
At a higher level, SvelteKit was built in response to the rise of the “unbundled development” workflow in which a dev server serves modules on demand instead of bundling the app. This makes startup virtually instantaneous, regardless of the size of your app.
Finally, SvelteKit will support all major serverless providers and include an adapter API to make it compatible with platforms it doesn’t officially cater to.
Though it’s still in public beta at the time of writing, SvelteKit now has thorough documentation and appears to be hurtling toward version 1.0.
Sapper, on the other hand, will no longer be maintained. As stated in the official docs:
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.
Sapper is the companion component framework to Svelte that helps you build larger and more complex apps in a fast and efficient way.
In this modern age, building a web app is a fairly complex endeavor, with code splitting, data management, performance optimizations, etc. That’s partly why there are myriad frontend tools in existence today, but they each bring their own level of complexity and learning curves.
Building an app shouldn’t be so difficult, right? Could it be simpler than it is right now? Is there a way to tick all the boxes while retaining your sanity? Of course there is — that was a rhetorical question!
Let’s start with the name: Sapper. I’ll just go ahead and quote the official docs on why the name was chosen:
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:
I’m sure you’d agree that managing those yourself could quickly become a chore distracting you from the actual business logic.
But talk is cheap — code is convincing! Let’s walk through a small server-rendered app using Svelte and Sapper.
Instead of me telling you how Sapper helps you build apps easily, we are going to explore the demo app you get when you scaffold a new project and see how it works behind the scenes.
To get started, run the following commands to bootstrap a new project:
$ npx degit "sveltejs/sapper-template#rollup" my-app $ cd my-app $ npm install $ npm run dev
Doing 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!
Sapper is an opinionated framework, meaning certain files and folders are required, and the project directory must be structured in a certain way. Let’s look at what a typical Sapper project looks like and where everything goes.
Every Sapper project has three entry points along with a 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
We need a server to serve our app to the user, don’t we? Since this is a Node.js environment, there are tons of options to choose from. You could use an Express.js server, a Koa.js server, a Polka server, etc., but there are some rules to follow:
/static
folder. Sapper doesn’t care how you do it. Just serve that folder!sapper.middleware()
imported from @sapper/server
.process.env.PORT
.Just three rules — not bad, if you ask me. Take a look at the src/server.js
file generated for us to see what it looks like in practice.
service-worker.js
If you need a refresher on what Service Workers are, this post should do nicely. Now, the 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
This is the main entry point for your app, where all your components, style refs, and scripts are injected as required. It’s pretty much set-and-forget except for the rare occasion when you need to add a module by linking to a CDN from your HTML.
routes
The MVP of every Sapper app. This is where most of your logic and content lives. We’ll take a deeper look in the next section.
If you ran all the commands in the Hands-on experience section, navigating to 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.
When you navigate to a page — say, /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?
In the previous section, we saw that it’s possible to have a [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.Server-side rendering (SSR) is a big part of what makes Sapper so appealing. If you don’t know what SSR is or why you need it, this article does a wonderful job of explaining it.
By default, Sapper renders all your apps on the server side first before mounting the dynamic elements on the client side. This allows you to get the best of both worlds without having to make any compromises.
There is a caveat to this, though: while Sapper does a near-perfect job of supporting third-party modules, there are some that require access to the 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.
We’ve seen how intuitive and simple it is to work with Sapper. The routing system is very easy to grasp even for absolute beginners, creating an API to power your frontend is fairly straightforward, SSR is very easy to implement, etc.
There are a lot of features I didn’t touch on here, including preloading, error handling, regex routes, etc. The only way to really get the benefit is to actually build something with it.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
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 nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
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..