Introduction
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 article, we’ll be taking a high-level look at Sapper, how it helps you build full-fledged, lightweight apps, and break down a server-rendered app. By the end of this article, you should know enough of Sapper to understand what makes it awesome.
With that said, it’s still a good idea to go through the documentation, as there are some concepts covered there that I left out.
What is Sapper?
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, what with code splitting, data management, performance optimizations, etc. That’s partly why there is a myriad of 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.
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:
- Routing
- Server-side rendering
- Automatic code splitting
- Offline support (using Service Workers)
- High-level project structure management
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 + Sapper.
Hands-on experience
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!
Project structure
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.
Entry points
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:
- The server must serve the contents of the
/static
folder. Sapper doesn’t care how you do it. Just serve that folder! - Your server framework must support middlewares (I personally don’t know any that don’t), and it must use
sapper.middleware()
imported from@sapper/server
. - Your server must listen on
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.
Routing
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.
Page routes
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?
Server routes
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:
- Page routes are
.svelte
files under thesrc/routes
folder that render content to the browser. - Server routes are
.js
files that contain API endpoints and are mapped to specific page routes by name. - Page routes can call the endpoints defined in server routes to perform specific actions like fetching data.
- Sapper is pretty well-thought-out.
Server-side rendering
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.
Conclusion
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.
Now that you understand the basics of Sapper, it’s time for you to go forth and play around with it. Create a small project, break things, fix things, mess around, and just get a feel for how Sapper works. You just might fall in love.
Plug: LogRocket, a DVR for web apps
LogRocket is a frontend logging tool that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.
The post Exploring Sapper + Svelte: A quick tutorial appeared first on LogRocket Blog.
Top comments (0)