Debjyoti Banerjee I'm Debjyoti, software engineer and game developer. Currently exploring JavaScript and Flutter, and trying to come up with solutions to problems in the healthcare sector. Love open source.

Exploring SvelteKit, the newest Svelte-based framework

11 min read 3131

Exploring the Newest Svelte-Based Framework: SvelteKit

The newest framework for creating web apps with Svelte is here: SvelteKit. This framework is easy to use even for less experienced developers.

SvelteKit is the successor to Sapper, a compact yet powerful JavaScript framework powered by Svelte. The new release of SvelteKit is an upgrade to what Sapper provides and is currently in public beta.

Exploring SvelteKit myself left me impressed by the fact that it was quite easy to understand; it has fewer concepts to learn compared to other popular frameworks like React.

Let’s delve into the basics of Svelte and SvelteKit and eventually explore a SvelteKit example.

What is Svelte and SvelteKit?

Svelte is a component library like React, and SvelteKit is the app framework like Next.js. While similar, the reason Svelte stands apart from React is because it provides a different way to think about web apps.

React uses virtual DOM diffing to decide the changes needed to update a UI, but Svelte is a compiler, which compiles your code and converts the Svelte components into JavaScript to render and update them, making it faster and lighter.

SvelteKit then does all the heavy lifting of setting up an app with server-side rendering, routing, and more, just like Next.js. However, SvelteKit also uses an adapter that can export your app to a specific platform and adapts well to serverless architecture. Since serverless architecture is becoming more prominent, it’s a good reason to try SvelteKit out.

You can use the official SvelteKit adapters for platforms like Netlify and Vercel.

By also providing features including server-side rendering, code splitting, and more, SvelteKit is especially useful for beginnings.

With that, let’s see how we can create a new project with SvelteKit.

We made a custom demo for .
No really. Click here to check it out.

Setting up SvelteKit

Before we code an example app, we’ll play with the demo app that you get when you create a new project with SvelteKit and review some key concepts that will familiarize you with the framework.

Installation

Begin with inputting the following code snippet into a terminal. This will set up an app in the current directory.

npm init [email protected]

Then input the following to install all the dependencies and we’re good to go.

npm install

Also, if you’re using Visual Studio Code, install the official Svelte extension for syntax highlighting and other features for writing Svelte components, such as app pages.

SvelteKit sets up a routing system where files in your src/routes determine the routes in your app. This directory can be changed by editing svelte.config.cjs.

Note that src/routes/index.svelte is the homepage.

By inputting npm run dev, you start a development server. SvelteKit uses Vite behind the scenes making updates are blazing fast.

At this point, install the static adapter to build the pre-rendered version of the entire app by using the following:

npm i -D @sveltejs/[email protected]

Now, let’s explore some code, make some changes, and see the result.

Routing

We will add another route to the counter app that SvelteKit bootstrapped for us by inputting about.svelte to the src/routes/ directory.

<!-- about page -->
<svelte:head>
    <title>About</title>
</svelte:head>
<h1>About Page</h1>
<p>This is the about page. Click <a href="/">here</a> to go to the index page.</p>

As you can probably guess, this will set up another route for us at /about. To navigate to this page, we will add a link to the index page as well.

The index page already has the following line:

<p>Visit <a href="https://svelte.dev">svelte.dev</a> to learn how to build Svelte apps.</p>

We’ll just change it to the code below:

<p>Visit the <a href="/about">about</a> page</p>

When we click on the link, the internal router kicks in and handles the navigation. In fact, SvelteKit handles the navigation by default. The initial load is handled on the server side, then SvelteKit’s inbuilt router handles the subsequent navigation on the client side unless we specify otherwise.

SvelteKit allows you to disable this router by altering the Svelte configuration file svelte.config.cjs. Setting the router property to false disables the app-wide router. This will cause the app to send new requests for each page, meaning the navigation will be handled on the server side.

You can also disable the router on a per-page basis if needed. We’ll go ahead and see it in action by adding the following to the top of about.svelte:

<script context="module" lang="ts">
    export const router=false;
</script>

I’ll talk about the context="module" and lang="ts" in a bit. For now, let’s run the app by npm rundev. We should expect all routing from the About page will be handled by the server, meaning when navigating from the About page, new requests to the server will be made. This is a fine little functionality SvelteKit provides us completely out of the box.

Scripts and styles

Looking at the script we were just working with, the scripts containing context="module" are added directly to the module. This means they run once whenever the component is initialized as opposed to other scripts without context="module", which become a part of the instance — the component — and run whenever an instance is created and initialized.

So, variables in <script context="module"></script> are shared among the instances of the default export of the module, which is the component itself.

The lang="ts" tells the compiler that the language used is TypeScript. You need to use this if you chose TypeScript as the language during setup. If you’re using JavaScript, then there’s no need to do anything here.

As a little experiment, we’ll add this to the top of src/lib/Counter.svelte:

<script context="module">
    console.log("module code");
</script>

And then, add this line to the top of the already present instance-level script:

console.log("component code");

We’ll also include another counter component in index.svelte by adding <Counter/>.

So, what do we see when we run this? Since the two counters are independent of each other, the logs show that “module code” ran first, then the two “component code” messages appear.

Now, let’s add this to the bottom of about.svelte:

<style>
    p {
        color:blue;
        max-width: 14rem;
        margin: 2rem auto;
        line-height: 1.35;
    }
</style>

In Svelte, styles applied to components are scoped to the component. This style will only be applied to the About page.

You’ll also notice the $layout.svelte component inside routes/; this can display and style things that are persistent across different routes, like the footer, for example.

Layout

Let’s dive into how the layout component can wrap every component within itself, making it an ideal place to perform functions like providing the store and setting up the context.

First, let’s add this to the $layout.svelte file:

<script>
  console.log("layout component");
</script>  

Then add similar logging statements to the routes index.svelte and about.svelte. Start the development server, and look at the console in your browser; the layout message appears first and then the index message.

Console In Your Browser Shows The Layout Message First Then The Index Message.

Now when we navigate to the About page, the logs show the added about component line

The About Page Logs Show the Added About Component Line

As the $layout component is rendered first, the pages are added and removed from the layout as they are needed by the router.

You can also use the lifecycle method onDestroy, that Svelte provides to verify that the layout component renders only once and is never unmounted on navigating to different pages. By adding these lines to $layout.svelte, you’ll notice that no log appears in the console:

import { onDestroy } from 'svelte';
onDestroy(() => console.log("$layout unmounted")); 

onDestroy never gets called even when we navigate between pages.

We can use this behavior to our advantage by fetching some data that many pages need or setting up a centralized store (which we will see later) that other pages can use to pass data to each other.

If you’re familiar with Svelte or React, adding context to the code saves us from prop drilling. In our example, we can add context for data in $layout.svelte for all the pages and their components to receive.

The server side

We know that SvelteKit, by default, renders the app on the server side during the first load. But what if we wanted to populate our app with data during SSR without showing the users a loading spinner? Or, how do we pass data from the server to the client side?

Well, SvelteKit provides hooks that run only on the server and help us achieve these goals. But before we explore hooks, I want to talk about endpoints to better understand the server side.

Endpoints are server-side and are created similarly to pages and routes. However, files that are endpoints will end with a .js or .ts extension in the routes directory.

// src/routes/dogs.ts
import type { RequestHandler, Response } from "@sveltejs/kit";

interface dog{
name: string
}
const dogs:dog[]=[{name:"German Shepherd"},{name:"BullDog"},{name:"Poodle"}]
export const get:RequestHandler= async () =>{
    const res:Response={
        body:{
            dogs
        }
     }
    return res;
}

The method name get corresponds to the HTTP method GET. This endpoint is available at /dogs. If you navigate to /dogs in your browser, you will find a JSON response containing the list of dogs.

With hooks, you have finer control over the server side, creating an ideal place to perform functions like authentication because they also receive the HTTP request object from the client. There are three hooks in SvelteKit, and we will be using getContext and getSession in the next section.

Building in SvelteKit

Understanding the basics of the SvelteKit ecosystem, we can build a very simple toy application that will fetch data from a source that we’ll set up, perform some simple authentication, and set up a central store.

Our app will contain the following routes: /counter1 , /counter2 , /about, and /login. The Counter pages will be protected and the About page will not.

So let’s focus on the authentication logic first.

Authentication

Since the hooks run on the server on each request before anything else runs, and because they have access to the request parameters, src/hooks.ts is the ideal place to extract cookies and create a session for the user.

Note that the session is not a session in its typical sense; the server side will not keep any record of the sessions. The session we will use here will simply help us pass data to the client side and provide the initial state.

The getContext hook receives the request headers, which may or may not contain cookies, depending on the authentication of a request. When we extract the authentication token and return it, the next hook will receive this context as a parameter.

Anything returned from the getSession hook is available to every page as a session variable.

// src/hooks.ts
import {defaultState} from '$lib/store';
import * as cookie from 'cookie';
const auth_token='demo_token_for_example';
const userDetails={name:"Deb",age:45}

export const getContext:GetContext=({ headers })=>{
    const cookies = cookie.parse(headers.cookie || '');
    return {
        token:cookies['token']
    };
}
export const getSession:GetSession=async ({context})=>{
    let initialState={...defaultState};
    if (context['token']===auth_token){
        console.log("tokens match");
        initialState.authenticated=true
        initialState.user=userDetails;
    }
    console.log(initialState)
    return initialState
}

For the sake of brevity and simplicity, we’ll store the authentication token and user details in the file itself. In a real project, you would probably use a database for this or an authentication backend.

The idea is to extract a cookie from the headers in getContext then check if it has the right token. If it contains the right token, we return the “authenticated” initial state. Don’t worry about the initialState, we’ll take a look at $lib/store later in this post.

We’ll now set up an endpoint that will accept a GET request and return a cookie containing the token. This will be useful in the login component.

// src/routes/auth.ts
const auth_token='demo_token_for_example';
const cookie=`token=${auth_token};HttpOnly;Secure`
const header:Headers={'set-cookie':cookie}
export const get:RequestHandler=()=>{
    return{
        headers:header,
        body:{
            token:auth_token,
            success:true,
            user:{
                name:"Deb",
                age:45
            }
        }
    }

}

Again, the user details will be typically fetched from a database. But here, we’re hardcoding them for simplicity.

Building the store

If you’re not familiar with Svelte’s writable stores, they can be written to and from anywhere within the app and are reactive. This is a simple way to set up a writable store that will store the global state of our application.

// src/lib/store.ts
import {Writable, writable} from 'svelte/store';
export type User={
    name:string|null,
    age?:number
}
export interface stateType{
    authenticated:boolean,
    user:User,
    counter:number
}
export const defaultState:stateType={
    authenticated:false,
    user:{
        name:null,
    },
    counter:0
}
export default class Store{
    state:Writable<stateType>;
    constructor(initialState:stateType=defaultState){
        this.state=writable({...initialState})
    }
    changeAuthenticationState=(user:User)=>{
        this.state.update((obj)=>{
            console.log("old state")
            console.log(obj)
            return {
                ...obj,
                authenticated:!obj.authenticated,
                user:user
            }
        })
    }
    updateCounter=(val:number)=>{
        this.state.update((obj)=>{
            return {
                ...obj,
                counter:val
            }
        })
    }
}

Next, we’ll set up a context at the $layout.svelte root and provide our store to all the descendants, enabling all the pages to access store.

<!-- src/routes/$layout.svelte -->
<script context="module" lang="ts">
    import Store from '$lib/store';
    import {setContext} from 'svelte';
</script>
<script lang="ts">
    import '../app.css';
    import {session} from '$app/stores';
    const store=new Store($session)
    setContext<Store>('store',store);
</script>
<slot />

Notice how we’re creating a new store using the initial state we received from the session and passing it to setContext. The store can now be accessed in any page by the key 'store'.

The load function

Our pages can also export a special function called the load function. This function can fetch data or write to the session before the component renders, first running on the server side and then on the client side. This is especially useful during server-side rendering, as we might need to populate our page with data that must be fetched beforehand.

<!-- src/routes/login.svelte -->
<script context="module" lang="ts">
    import type { Load } from '@sveltejs/kit';
    export const load:Load=async ({session})=>{

                if(session.authenticated){
                    return{  
                        redirect:'/counter1',
                        status:302
                    }
                }   
            return {}
    }
</script>
<script lang="ts">
    import type Store from '$lib/store';
    import {goto} from '$app/navigation';
    import {setContext,getContext} from 'svelte';
    const store=getContext<Store>('store');
    const login=async ()=> {
        let res= await fetch('/auth');
        let data=await res.json();
        if(data.success){
            store.changeAuthenticationState(data.user);
            goto('/counter1');
        }
    }
</script>
<h1>Login Page</h1>
<button on:click={login}>Login</button>

In the load function of the Login page, we can check whether the user is authenticated since we don’t want to display the Login page to the authenticated user.

If they are authenticated, we redirect them to the /counter1 page. If not, we fetch the token and update the state. Once authenticated, we can navigate to the protected routes like the /counter1.

The counters

The load function of counter1.svelte checks if the user is authenticated and redirects them to the Login page if they are not. We perform this check only on the server side since our app is structured in a way that it does not provide any way to navigate to the /counter1 page without performing a full request to the server.

<script context="module" lang="ts">
    import {browser} from '$app/env';
    export const load:Load=async ({session})=>{
        if(!browser)
            {
                if(!session.authenticated){
                    return{ 
                        redirect:'login',
                        status:302
                    }
                }
                else{
                    session.counter=1; //set counter to 1 during ssr
                }
            }
            return {}
    }
</script>
<script lang="ts">
    import type Store from '$lib/store';
    import Counter from '$lib/Counter.svelte';
    import {setContext,getContext} from 'svelte';
    const store=getContext<Store>('store');
    const state=store.state;
</script>
<svelte:head>
    <title>Counter 1</title>
</svelte:head>
<main>
    <h1>Hello {$state.user.name}</h1>
    <Counter update={store.updateCounter} count={$state.counter}/>
    <p>Visit <a href="/counter2"> Counter2</a> </p>
</main>

However, we don’t include links to the protected pages in any unprotected page, so there’s no way to navigate to these without a full load. This means a request to the server will be made.

When a request for /counter1 is made, getSession runs and assigns the initial state, which sets the counter to 0. The load function then runs and updates the counter value to 1, sending the updated session to the layout component to set up the store with the updated state.

Note that if we had a load function in $layout.svelte, it would run before the load function of counter1.svelte.

The /counter2 page is the same as /counter1 except we initialized the counter to 2, prompting Line 13 to become session.counter=2.

In the following code, we can use the counter component in both the /counter1 and /counter2 pages:

<!-- Counter.svelte -->
<script lang="ts">
    export let count:number;
    export let update:Function;
    const increment = () => {
        update(count+1)
    };
</script>
<button on:click={increment}>
    Clicks: {count}
</button>

Finishing Up

To finish up the app, we must add the about.svelte page:

<!-About.svelte -->
<h1> About page </h1>

Creating a production build

npm run build will create a production build for us. Since we’re using the default node adapter, we get a node server in /build and serve the app using node build.

Conclusion

By using SvelteKit, we were able to create an app containing SSR, authentication, and a store in just a few minutes!

Since SvelteKit’s framework is still in beta, it can be difficult to find answers if you face any problems while working with it. However, if it suits your project requirements, it can be incredibly effective.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution 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.

.
Debjyoti Banerjee I'm Debjyoti, software engineer and game developer. Currently exploring JavaScript and Flutter, and trying to come up with solutions to problems in the healthcare sector. Love open source.

Leave a Reply