Skip to main content

A Beginner’s Guide to SvelteKit

By Erik Kückelheim
JavaScript
Share:

SvelteKit is an officially supported framework, built around Svelte. It adds key features to a Svelte app — such as routing, layouts and server-side rendering — and makes front-end development outrageously simple.

In this tutorial, we’ll take a beginner-friendly look at both Svelte and SvelteKit and build out a simple web app showing profile pages of imaginary users. Along the way, we’ll look at all the main features that SvelteKit has to offer.

Let’s start by looking at what Svelte brings to the table.

The Benefits of Working with Svelte

Svelte is growing in popularity, and that’s for a good reason. Developing apps with Svelte is based on writing reusable and self-contained components — similar to other popular JavaScript frameworks such as React.

The big difference comes with its build-time compilation — as opposed to a run-time interpretation of the code. In other words, Svelte already compiles your code during the build process and the final bundle only contains JavaScript that your application actually needs. This results in fast web apps with small bundle sizes.

Other frameworks only parse and bundle up the code you’ve written, essentially taking the component tree as is and shipping it to the client. In order for the browser to be able to interpret it and update the UI, a lot more code needs to be delivered and additional work is done on the client side. (You can read here how React handles this process under the hood.)

Other than that, Svelte is an ideal framework for beginners. Everyone who knows how to write HTML and how to include <style> and <script> tags with basic JavaScript and CSS can already start writing Svelte components.

So, Why Do I Need SvelteKit?

While Svelte alone gives you a very good development experience, you still have to decide on how you want to ship your application to the user. The classical approach would be to take your favorite module bundler like webpack or Rollup and bundle your code into one big, fat JavaScript file. Then, you’d call it from a very basic HTML document, like so:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    ...
  </head>

  <body>
    <!-- the entry point of your application -->
    <div id="app" />
    <!-- load the JavaScript that takes care of the rest -->
    <script src="dist/bundle.js"></script>
  </body>
</html>

While this is absolutely legit, the user’s experience might not be ideal. There are many touchpoints for improvement and this is where SvelteKit comes into play.

First of all, instead of serving an almost empty HTML file to the client, SvelteKit already comes with all the HTML elements you need for the first page view. The benefits are faster page loads and SEO boosts. There are two ways SvelteKit does this: prerendering and server-side rendering. I’ll explain both in more detail below. What stays the same is that once the JavaScript has been loaded, it takes over and enables typical features of a single page application, like client-side routing.

The second obvious difference between SvelteKit and a classical single JavaScript bundle is code-splitting. Instead of serving the entire app in one single Javascript file, SvelteKit splits the code into separate smaller chunks. Each chunk represents a route of your application. For example, everything that needs to be fetched for the /home and for the /about routes will be loaded once the user actually needs it — or a little bit earlier if you make use of SvelteKit’s prefetching functionality (like we’ll do below).

Another outstanding benefit of SvelteKit is that you can decide in which deployment environment your app is going to run. Nowadays, front-end developers have a variety of different platforms where applications can run. There are hosting providers for simple static files, more advanced serverless options such as Netlify, or server environments where Node servers can be executed, and so on. With tiny plugins called adapters, you tell SvelteKit to optimize your output for a specific platform. This greatly facilitates app deployment.

However, the biggest advantage SvelteKit has to offer is its ease of use. Of course, you can manually set up your build process from scratch with all these features, but this can be tedious and frustrating. SvelteKit makes it as easy as possible for you, and the best way to experience this is by actually using it.

This is why we’ll create a simple web app showing profile pages of made-up users. And along the way, we’ll look at all the features I’ve mentioned above in more detail.

Prerequisites

No previous knowledge is required, although some experience with Svelte might be helpful. The article “Meet Svelte 3, a Powerful, Even Radical JavaScript Framework” provides a good introduction.

To work with SvelteKit, you’ll need a working version of Node on your system. You can install it using the Node Version Manager (nvm). (You can find some setup instructions here.)

Please be aware that SvelteKit is (at the time of writing) still in beta, and some features might be subject to change. You can find all the code for this tutorial on GitHub.

Getting Started

To begin with, we initiate a new SvelteKit project. Execute the following commands in your terminal:

npm init svelte@next svelteKit-example-app

You’ll be asked a few questions so that you can customize your project. For our purposes, answer the following:

  • Which Svelte app template? -> SvelteKit demo app
  • Use TypeScript components -> no
  • Add ESLint for code linting? -> no
  • Add Prettier for code formatting? -> no

This will load a SvelteKit development environment including a functional example application.

In your project route there are now some configuration files: your package.json, the static folder, and the src folder. We’ll be working mainly inside the src folder. It has the following structure.

src
├── app.css
├── app.html
├── global.d.ts
├── hooks.js
├── lib
│   ├── Counter
│   │   └── index.svelte
│   ├── form.js
│   └── Header
│       ├── index.svelte
│       └── svelte-logo.svg
└── routes
    ├── __layout.svelte
    ├── about.svelte
    ├── index.svelte
    └── todos
        ├── _api.js
        ├── index.json.js
        ├── index.svelte
        └── [uid].json.js

The /src/app.html file is your app-shell, a minimal HTML page where your rendered HTML will be inserted and your bundle files linked from. Usually you don’t have to touch this file. You can insert some app-wide meta tags if you want to, but this isn’t necessary — as you will see in a moment.

The /src/routes folder is the heart of your application. The files inside this folder define the routes of your app. There are two types of routes: pages and endpoints. pages are Svelte components and are indicated by the .svelte extension. For example, a component named /src/routes/test.svelte would be served under the route /test. endpoints are normal JavaScript (or TypeScript) files and enable you to generate HTTP endpoints to fetch data.

Svelte components can have child components. For example, the route component /src/routes/test.svelte might import a component named Button.svelte. The place where you would store all your child components is the /src/lib folder.

Let’s see how all this works in action. Change into the newly created directory, then install the dependencies and start the app in development mode:

cd svelteKit-example-app
npm install
npm run dev -- --open

This will open the preexisting example app in a new browser tab. Click through the app and assure yourself it’s working.

Some preparation

As polished as the demo app is, it contains a bunch of files that we won’t need. Let’s get rid of those.

Delete the contents of the lib folder:

rm src/lib/*

Delete the routes/todos folder:

rm -rf src/routes/todos

We can do without the demo app’s styling. In the root of the project, open app.css and replace the contents with the following:

:root {  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;}
body {  margin: 0;}

Finally, open src/index.svelte and replace the contents with the following:

<main>
  <h1>HOME</h1>
</main>

With that done, let’s get to building out our demo.

Layouts and Client-side Routing

As I wrote above, every Svelte component in the routes folder defines one route. However, there’s one exception: the layout component, named __layout.svelte. This component contains code that applies to every single page of your app.

Let’s open the existing /src/routes/__layout.svelte file. All it does for now is import some app-wide CSS code. The <slot> element wraps the rest of the application. Let’s replace the content with the following:

<script>
  import "../app.css";
</script>

<svelte:head>
  <meta name="robots" content="noindex" />
</svelte:head>

<nav>
  <a href=".">HOME</a>
  <a href="/about">ABOUT</a>
</nav>

<slot />

<style>
  nav {
    padding: 1rem;
    box-shadow: -1px 1px 11px 4px #898989;
  }
  a {
    text-decoration: none;
    color: gray;
    margin-right: 1rem;
  }
</style>

Note: if you want to have syntax highlighting for Svelte files, there are extensions you can install. This one is good for VS Code.

In this example, we used the <svelte:head> element to define meta tags that will be inserted in the <head> of our document. Since we did this in the layout component, it will be applied to the entire app. The robot tag is just an example.

Furthermore, we created a navbar. This is a typical use case for the layout component, as it’s usually intended to be shown on every page of your application.

The navbar has two links: one to the root of the application — which already has content served by the /src/routes/index.svelte component — and one to the about page. The about page was also created by the demo app. Open it and replace its content with the following:

<main>
  <h1>ABOUT</h1>
  <hr />
  <div>A website to find user profiles</div>
</main>

<style>
  main {
    font-size: 1.5rem;
    margin: 4rem;
    padding: 2rem;
    color: gray;
    justify-content: center;
    box-shadow: 4px 5px 11px 10px lightgray;
  }
</style>

This page is pretty basic. We included some HTML and applied some styling.

Let’s go back to the browser and navigate to the new page. Our modifications should already be visible and you should see something like this:

About Page

Let’s navigate between the landing page and the about page. You might realize that changing the page doesn’t refresh the entire application. The navigation feels smooth and instant. This is because SvelteKit applies Client-Side Routing out of the box. Although we used normal <a> tags in our navbar, SvelteKit identifies those as internal links and intercepts them using its built-in client router.

Static Pages and Prerendering

As I described above, SvelteKit uses the concept of adapters to build apps for different environments. Adapters are imported in the svelte.config.cjs file.

When you open this configuration file, you can see that our application currently uses the node adapter. This will optimize the build output for a Node environment and, by default, every page of our application will be rendered upon request by a Node server. However, this seems a little bit too much, considering the current state of our app. Also, you might not want to run a server for your application.

As our app doesn’t currently depend on any dynamic data, it could consist entirely of static files. And there’s an adapter-static that you can install, which turns SvelteKit into a static site generator. It would render your entire app into a collection of static files during the build process. However, this would prevent us from creating additional pages that depend on server-side rendering.

As we don’t want to turn all our pages into static files, we’ll make use of another SvelteKit feature which enables us to prerender individual files of our application. In our case, we’d like the about page to be prerendered, since it consists of static content and rendering the page on every request would be unnecessary. We can achieve this by adding the following code snippet at the top of our /src/routes/about.svelte page:

<script context="module">
  export const prerender = true;
</script>

We can test this out by running npm run build. This will generate a functioning Node server inside the /build folder. As you can see, there’s an HTML file /build/prerendered/about/index.html containing the prerendered HTML for the about page. There’s no HTML file for our landing page since it will be rendered by the Node server upon request.

You can run the generated Node server with node build/index.js.

Endpoints

Now it’s time to fill our page with some dynamic content. We’ll adjust the landing page such that it shows a list of user avatars. To do so, we need to fetch a list of user information from an API endpoint. Most developing teams have a separate back end. That would be the place to go. However, SvelteKit makes it easy to turn your application full stack using endpoint pages. Since we have no back end, we’ll create such a page.

Instead of using a real database, we’ll generate some mock user data. To do so, we’ll use the library faker. Let’s install it with npm install -D faker.

Now, create a file /src/routes/api/index.js in a new /api folder. Since the file has no .svelte extension, it will be treated as an endpoint. The syntax /api/index.js is the same as api.js. The endpoint will become available under /api. Insert the following code:

import faker from "faker";

const generateUsers = () =>
  [...Array(50)].map(() => {
    const lastName = faker.name.lastName();
    return {
      avatar: `https://avatars.dicebear.com/api/human/${lastName}.svg`,
      lastName,
    };
  });

export async function get() {
  return {
    body: generateUsers(),
  };
}

This file exports a function get. As you might already have guessed, it corresponds to the HTTP method GET. All it does is return an object with property body that holds an array of user data created with generateUsers.

The function generateUsers returns an array of 50 objects with properties lastName and avatar. lastName is generated using faker. avatar stores a URL that points to the free DiceBear Avatar API. It generates random avatars using a seed value, which is in our case lastName.

If we had a real database, we could replace generateUsers with something like findUsers and access the database inside this function.

That’s all it needs. Go back to the browser (make sure your app is still running in dev mode npm run dev) and navigate to http://localhost:3000/api. This will load the raw data. Note that creating an endpoint like we did is only necessary if you don’t have a separate back-end API to fetch data.

Fetching Data with the load Function

Next, we’ll use the new endpoint to display user data on our landing page. Open the existing /src/routes/index.svelte page and replace its content with the following:

<script context="module">
  export async function load({ fetch }) {
    const res = await fetch('/api');

  if (res.ok) return { props: { users: await res.json() } };
  return {
    status: res.status,
    error: new Error()
   };
  }
</script>

<script>
  export let users;
</script>

<main>
  {#each users as { avatar, lastName }}
  <a href={`/${lastName}`} class="box">
    <img src={avatar} alt={lastName} />
    <h2>{lastName}</h2>
  </a>
  {/each}
</main>

<style>
  main {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  }
  .box {
  padding: 0.25rem;
  margin: 1.5rem;
  color: salmon;
  box-shadow: 4px 5px 11px 2px lightgray;
  }
  .box:hover {
  box-shadow: 4px 5px 11px 10px lightgray;
  }
  img {
  width: 15rem;
  object-fit: contain;
  }
</style>

The key challenge to fetching data for dynamic content on a page is that there are two ways a user can navigate to it. The first way is from external sources or after a page refresh. This would cause the application to be loaded from scratch and the page to be served by the server. The second way is from internal navigation, in which case the page would be served by the JavaScript bundle on the client side. In the former, the data is fetched by the server, while in the latter, it’s fetched by the client.

SvelteKit offers a very elegant solution for this — the load function. The load function can run both on the client and on the server side and in both cases will be executed before the component renders. This is why we have to place it inside a <script> element with context="module".

load receives an object with property fetch that we can use to fetch data. It behaves identically to the native fetch API. In this example, we use our new endpoint /api to fetch the array of user objects. To pass this data to our component, we return an object with the props property, which stores our user array.

If you had a separate back-end API, instead of fetching data from our /api endpoint, you’d fetch it within the load function from the back end.

In case load runs on the server, the client will realize that the data has already been fetched and will not make an additional request.

Since we returned a props object, our component can access those props in the normal Svelte way — with export let inside a <script> tag. This is what we do to access our users.

Next, we visualize all our 50 users using the each syntax that we know from Svelte. Inside the each block, we have access to a user’s avatar and lastName properties. We use avatar as the value for the src attribute of an <img> tag.

Now your landing page should look like this:

Landing Page

Dynamic Parameters

Each user box on our landing page is an internal link with route /[lastName]. This is where dynamic parameters come into play. Under the route /[lastName], we’ll display additional information for the respective user. To do this, we’ll first have to extend our API with an additional endpoint for fetching individual user data.

Create a new file /src/routes/api/[lastName].js with the following content:

import faker from "faker";

export async function get({ params }) {
  const { lastName } = params;
  return {
    body: {
      lastName,
      firstName: faker.name.firstName(),
      avatar: `https://avatars.dicebear.com/api/human/${lastName}.svg`,
      title: faker.name.title(),
      phone: faker.phone.phoneNumber(),
      email: faker.internet.email(),
    },
  };
}

Notice the dynamic parameter [lastName] in the file name. We can access this parameter from the params property of the get function. We use it to return the correct values for lastName and avatar in the body object. Next, we generate some additional mock data for this user with faker that we also return within the body object.

We can test this endpoint with an arbitrary lastName value. Open the browser and navigate to http://localhost:3000/api/Spiderman. This will load the raw data for an arbitrary user with a value Spiderman of lastName.

Next, we create a new page — /src/routes/[lastName].svelte — with the following content:

<script context="module">
  export async function load({ fetch, page }) {
    const { lastName } = page.params;
    const res = await fetch(`/api/${lastName}`);

    if (res.ok) return { props: { user: await res.json() } };
    return {
      status: res.status,
      error: new Error(),
    };
  }
</script>

<script>
  export let user;
</script>

<main>
  <h1>{user.firstName} {user.lastName}</h1>
  <div class="box">
    <img src="{user.avatar}" alt="{user.astName}" />
    <ul>
      <li>Title: {user.title}</li>
      <li>Phone: {user.phone}</li>
      <li>Email: {user.email}</li>
    </ul>
  </div>
</main>

<style>
  main {
    margin: 4rem;
    padding: 2rem;
    color: gray;
    justify-content: center;
    box-shadow: 4px 5px 11px 10px lightgray;
  }
  h1 {
    color: salmon;
  }
  .box {
    display: flex;
    font-size: 1.5rem;
  }
  img {
    width: 15rem;
    object-fit: contain;
    margin-right: 2rem;
  }
  li {
    margin-bottom: 1rem;
  }
</style>

Note again the dynamic parameter [lastName] in the file name. We can access it using the page property that the load function receives.

Again, we use fetch to access our new endpoint /api/[lastName] and pass the user data as property user to the Svelte component. We access this property with export let user and visualize the data with some basic Svelte syntax.

Now you should be able to navigate back to the landing page and click on any user box. This will open the corresponding user page. You should see something like this:

User Page

Prefetching

There’s one last feature that I’d like to show, and I am really excited about it. SvelteKit offers the possibility to prefetch data for individual pages.

Let’s go back to our /src/routes/index.svelte page and add the attribute sveltekit:prefetch to the <a> tag. Like so:

<a sveltekit:prefetch href={`/${lastName}`} class="box">

This tells SvelteKit to execute the load function of the corresponding page upon hovering the <a> element.

Try it out by opening the network tab in your browser (see below). Every time you hover over one of the user boxes, a request to /api/[lastName] is made and the data for the corresponding user page is fetched. This saves additional milliseconds and ensures a better user experience.

SvelteKit Prefetching

By the way, this is also a great way to see how SvelteKit applies code splitting out of the box. Reload the page and clear the Network log. Note that the very first time you hover over an avatar, one JavaScript and one CSS file is being loaded. This is the code chunk corresponding to our /src/routes/[lastName].svelte page. It gets loaded only once per page session. If you hover over another avatar, only the corresponding data gets loaded, but not again the JavaScript and CSS.

You don’t have to necessarily apply the prefetching attribute to the <a> tag. If you prefer, you can do the prefetching programmatically using the prefetch function of SvelteKit’s $app/navigation module.

Conclusion

Working with SvelteKit feels very intuitive. All in all, it took me only about an hour to learn all the main features and the results are absolutely astonishing. You get blazing-fast, SEO-optimized web apps that provide you the best user experience that modern build tools can possibly deliver.

By default, SvelteKit renders your page on the server. On the client it gets progressively enhanced by a highly optimized JavaScript bundle to enable client-side routing. With a few lines of code you can prerender individual pages or prefetch data to enable instant page load and navigation. Features like code splitting ensure that Svelte’s advantage of small compilation output doesn’t get mitigated by large app-wide bundles.

Last but not least, SvelteKit gives you complete freedom with respect to all its features. There’s always a way to exclude a feature if you prefer to.

SvelteKit together with Svelte itself is a real game changer to me. And I believe it could be so for many others.

Self-taught developer living in Konstanz, Germany. Creator of JSchallenger.

Integromat Tower Ad