Menu

Introduction

Note: Sapper is in early development, and some things may change before we hit version 1.

What is Sapper?

Sapper is a framework for building extremely high-performance web apps. You're looking at one right now! There are two basic concepts:

  • Each page of your app is a Svelte component
  • You create pages by adding files to the routes directory of your project. These will be server-rendered so that a user's first visit to your app is as fast as possible, then a client-side app takes over

Building an app with all the modern best practices — code-splitting, offline support, server-rendered views with client-side hydration — is fiendishly complicated. Sapper does all the boring stuff for you so that you can get on with the creative part.

You don't need to know Svelte to understand the rest of this guide, but it will help. In short, it's a UI framework that compiles your components to highly optimized vanilla JavaScript. Read the introductory blog post and the guide to learn more.

Why the name?

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.

Comparison with Next.js

Next.js is a React framework from Zeit, and is the inspiration for Sapper. There are a few notable differences, however:

  • Sapper is powered by Svelte instead of React, so it's faster and your apps are smaller
  • Instead of route masking, we encode route parameters in filenames (see the routing section below)
  • As well as pages, you can create server routes in your routes directory. This makes it very easy to, for example, add a JSON API such as the one powering this very page (try visiting /api/guide)
  • Links are just <a> elements, rather than framework-specific <Link> components. That means, for example, that this link right here, despite being inside a blob of markdown, works with the router as you'd expect

Getting started

The easiest way to start building a Sapper app is to clone the sapper-template repo with degit:

npm install -g degit
degit sveltejs/sapper-template my-app
cd my-app
npm install
npm run dev

This will scaffold a new project in the my-app directory, install its dependencies, and start a server on localhost:3000. Try editing the files to get a feel for how everything works – you may not need to bother reading the rest of this guide!

Sapper app structure

This section is a reference for the curious. We recommend you play around with the project template first, and come back here when you've got a feel for how things fit together.

If you take a look inside the sapper-template repo, you'll see some files that Sapper expects to find:

├ package.json
├ server.js
├ webpack.client.config.js
├ webpack.server.config.js
├ assets
│ ├ # your files here
├ routes
│ ├ # your routes here
├ templates
│ ├ 2xx.html
│ ├ 4xx.html
│ ├ 5xx.html
│ ├ main.js
│ └ service-worker.js

You'll notice a few extra files and a cypress directory which relates to testing — we don't need to worry about those right now.

You can create these files from scratch, but it's much better to use the template. See getting started for instructions on how to easily clone it

package.json

Your package.json contains your app's dependencies, including sapper, and defines a number of scripts:

  • npm run dev — start the app in development mode, and watch source files for changes
  • npm run build — build the app in production mode
  • npm start — start the app in production mode after you've built it
  • npm test — run the tests (see testing)

server.js

This is a normal Express app, with two requirements:

  • it should serve the contents of the assets folder, using for example serve-static
  • it should call app.use(sapper()) at the end

Beyond that, you can write the server however you like.

webpack.[type].config.js

Sapper uses webpack to bundle your app. There are two config files, one for the client, one for the server. You probably won't need to change the config, but if you want to (for example to add new loaders or plugins), you can.

assets

This is a place to put any files that your app uses — fonts, images and so on. For example assets/favicon.png will be served as /favicon.png.

Sapper doesn't serve these assets — you'd typically use serve-static for that — but it will read the contents of the assets folder so that you can easily generate a cache manifest for offline support (see service-worker.js).

routes

This is the meat of your app — the pages and server routes. See the section on routing for the juicy details.

templates/xxx.html

In here, you put HTML files corresponding to the possible responses Sapper can generate:

  • 2xx.html — everything working as planned
  • 4xx.html — client error (e.g. 404 Not Found)
  • 5xx.html — server error (e.g. 500 Internal Server Error)

Each file will contain placeholder tags, like %sapper.html%, which Sapper will fill in with data. The possible values for 2xx responses are:

  • %sapper.styles% — critical CSS for the page being requested
  • %sapper.head% — HTML representing page-specific <head> contents, like <title>
  • %sapper.html% — HTML representing the body of the page being rendered
  • %sapper.main% — the URL of the client-side app

For 4xx responses:

  • %sapper.status% — the HTTP status code
  • %sapper.title% — the name of the error
  • %sapper.method% — the HTTP method, e.g. GET or POST
  • %sapper.url% — the requested URL
  • %sapper.main% — the URL of the client-side app

For 5xx responses:

  • %sapper.status% — as above
  • %sapper.title% — as above
  • %sapper.error% — the text of the error
  • %sapper.stack% — a stack trace

templates/main.js

The entry module for the client-side app. This must import, and call, the init function from sapper/runtime/app.js:

import { init } from 'sapper/runtime/app.js';
init(document.querySelector('#sapper'), __routes__);

In many cases, that's the entirety of your entry module, though you can do as much or as little here as you wish. See the runtime API section for more information on functions you can import.

The __routes__ variable is injected by Sapper.

templates/service-worker.js

Service workers act as proxy servers that give you fine-grained control over how to respond to network requests. For example, when the browser requests /goats.jpg, the service worker can respond with a file it previously cached, or it can pass the request on to the server, or it could even respond with something completely different, such as a picture of llamas.

Among other things, this makes it possible to build applications that work offline.

Because every app needs a slightly different service worker (sometimes it's appropriate to always serve from the cache, sometimes that should only be a last resort in case of no connectivity), Sapper doesn't attempt to control the service worker. Instead, you write the logic in service-worker.js, and Sapper injects the following variables:

  • __assets__ — an array of files found in the assets directory
  • __shell__ — the client-side JavaScript generated by webpack, plus an empty index.html file created from 2xx.html that you can use to bypass server-side rendering if you wish
  • __routes__ — an array of { pattern: RegExp } objects you can use to determine whether a Sapper-controlled page is being requested

Routing

As we've seen, there are two types of route in Sapper — pages, and server routes.

Pages

Pages are Svelte components written in .html files. When a user first visits the application, they will be served a server-rendered version of the route in question, plus some JavaScript that 'hydrates' the page and initialises a client-side router. From that point forward, navigating to other pages is handled entirely on the client for a fast, app-like feel. (Sapper will preload and cache the code for these subsequent pages, so that navigation is instantaneous.)

For example, here's how you could create a page that renders a blog post:

<!-- routes/blog/[slug].html -->
<:Head>
  <title>{{post.title}}</title>
</:Head>

<h1>{{post.title}}</h1>

<div class='content'>
  {{{post.html}}}
</div>

<script>
  export default {
    // the preload function takes a `{ params, query }`
    // object and turns it into the data we need to
    // render the page
    preload({ params, query }) {
      // the `slug` parameter is available because this file
      // is called [slug].html
      const { slug } = params;

      return fetch(`/api/blog/${slug}`).then(r => r.json()).then(post => {
        return { post };
      });
    }
  };
</script>

When rendering pages on the server, the preload function receives the entire request object, which happens to include params and query properties. This allows you to use session middleware (for example). On the client, only params and query are provided.

Server routes

Server routes are modules written in .js files that export functions corresponding to HTTP methods. Each function receives Express request and response objects as arguments, plus a next function. This is useful for creating a JSON API. For example, here's how you could create an endpoint that served the blog page above:

// routes/api/blog/[slug].js
import db from './_database.js'; // the underscore tells Sapper this isn't a route

export async function get(req, res, next) {
  // the `slug` parameter is available because this file
  // is called [slug].js
  const { slug } = req.params;

  const post = await db.get(slug);

  if (post !== null) {
    res.set('Content-Type', 'application/json');
    res.end(JSON.stringify(post));
  } else {
    next();
  }
}

delete is a reserved word in JavaScript. To handle DELETE requests, export a function called del instead.

There are three simple rules for naming the files that define your routes:

  • A file called routes/about.html corresponds to the /about route. A file called routes/blog/[slug].html corresponds to the /blog/:slug route, in which case params.slug is available to the route
  • The file routes/index.html (or routes/index.js) corresponds to the root of your app. routes/about/index.html is treated the same as routes/about.html.
  • Files and directories with a leading underscore do not create routes. This allows you to colocate helper modules and components with the routes that depend on them — for example you could have a file called routes/_helpers/datetime.js and it would not create a /_helpers/datetime route

Subroutes

Suppose you have a route called /settings and a series of subroutes such as /settings/profile and /settings/notifications.

You could do this with two separate pages — routes/settings.html (or routes/settings/index.html) and routes/settings/[submenu].html, but it's likely that they'll share a lot of code. If there's no routes/settings.html file, then routes/settings/[submenu].html will match /settings as well as subroutes like /settings/profile, but the value of params.submenu will be undefined in the first case.

Runtime API

The sapper/runtime.js module contains functions for controlling your app and responding to events. More functions will be added in future versions.

init(selector, routes)

  • selector — a string representing the element to render pages to, typically '#sapper'
  • routes — an array of route objects generated by Sapper

This configures the router and starts the application — listens for clicks on <a> elements, interacts with the history API, and renders and updates your Svelte components.

Returns a Promise that resolves when the initial page has been hydrated.

import { init } from 'sapper/runtime.js';
init('#sapper', __routes__).then(() => {
  console.log('client-side app has started');
});

goto(href, [options])

  • href — the page to go to
  • options — can include a replaceState property, which determines whether to use history.pushState (the default) or history.replaceState). Not required

Programmatically navigates to the given href. If the destination is a Sapper route, Sapper will handle the navigation, otherwise the page will be reloaded with the new href. (In other words, the behaviour is as though the user clicked on a link with this href.)

prefetch(href)

  • href — the page to prefetch

Programmatically prefetches the given page, which means a) ensuring that the code for the page is loaded, and b) calling the page's preload method with the appropriate options. This is the same behaviour that Sapper triggers when the user taps or mouses over an <a> element with rel=prefetch.

Prefetching

Sapper uses code splitting to break your app into small chunks (one per route), ensuring fast startup times. After the first route renders, it will fetch the code for subsequent routes, so that they're already ready when the user wants to navigate to them.

For dynamic routes, such as our routes/blog/[slug].html example, that's not enough. In order to render the blog post, we need to fetch the data for it, and we can't do that until we know what slug is. In the worst case, that could cause lag as the browser waits for the data to come back from the server.

rel=prefetch

We can mitigate that by prefetching the data. Adding a rel=prefetch attribute to a link...

<a rel=prefetch href='/blog/what-is-sapper'>What is Sapper?</a>

...will cause Sapper to run the page's preload function as soon as the user hovers over the link (on a desktop) or touches it (on mobile), rather than waiting for the click event to trigger navigation. Typically, this buys us an extra couple of hundred milliseconds, which is the difference between a user interface that feels laggy, and one that feels snappy.

rel=prefetch is a Sapper idiom, not a standard attribute for <a> elements

Deployment

Sapper apps run anywhere that supports Node 8 or higher.

Deploying to Now

We can very easily deploy our apps to Now:

npm install -g now
now

This will upload the source code to Now, whereupon it will do npm run build and npm start and give you a URL for the deployed app.

For other hosting environments, you may need to do npm run build yourself.

Exporting

Many sites are effectively static, which is to say they don't actually need an Express server backing them. Instead, they can be hosted and served as static files, which allows them to be deployed to more hosting environments (such as Netlify or GitHub Pages). Static sites are generally cheaper to operate and have better performance characteristics.

Sapper allows you to export a static site with a single zero-config sapper export command. In fact, you're looking at an exported site right now!

Static doesn't mean non-interactive — your Svelte components work exactly as they do normally, and you still get all the benefits of client-side routing and prefetching.

This is an experimental feature — if you encounter unexpected behaviour or feel that there are missing features, please raise an issue!

sapper export

Inside your Sapper project, try this:

# npx allows you to use locally-installed dependencies
npx sapper export

This will create a dist folder with a production-ready build of your site. You can launch it like so:

npx serve dist

Navigate to localhost:5000 (or whatever port serve picked), and verify that your site works as expected.

You can also add a script to your package.json...

{
  "scripts": {
    ...
    "build": "sapper export"
  }
}

...allowing you to npm run build your app.

When not to export

The basic rule is this: for an app to be exportable, any two users hitting the same page of your app must get the same content from the server. In other words, any app that involves user sessions or authentication is not a candidate for sapper export.

Note that you can still export apps with dynamic routes, like our routes/blog/[slug].html example from earlier. sapper export will intercept fetch requests made inside preload, so the data served from routes/api/blog/[slug].js will also be captured.

Route conflicts

Because sapper export writes to the filesystem, it isn't possible to have two server routes that would cause a directory and a file to have the same name. For example, routes/api/blog/index.js and routes/api/blog/[slug].js would try to create dist/blog and dist/blog/some-slug, which is impossible.

The solution is to rename one of the routes to avoid conflict — for example, routes/api/blog-index.js. (Note that you would also need to update any code that fetches data from /api/blog to reference /api/blog-index instead.)

For pages, we skirt around this problem by writing dist/foo/index.html instead of dist/foo.

Testing

You can use whatever testing frameworks and libraries you'd like. The default in sapper-template is Cypress.

Running the tests

npm test

This will start the server and open Cypress. You can (and should!) add your own tests in cypress/integration/spec.js — consult the docs for more information.