How to Build an IMDB Clone Using Sapper

Sapper, the application framework by Svelte is built to provide a great developer experience and flexible filesystem-based routing, without compromising on SEO or performance, all with a small footprint.

It provides server-side rendering for fast initial page loads, but with the help of client-side JavaScript, navigation is just as fast as the initial load time.


The Goal of This Tutorial

The goal of this tutorial is to get you up and running with Sapper. We will recreate the top picks section on IMDB, and we will also let users find out more about a movie, by clicking on the cards.

The final app of this tutorial. You can get the full source code from GitHub.

We will go over the structure of Sapper, then we will see how navigation works between routes and how you can create endpoints to fetch data. This tutorial assumes that you already have a basic knowledge about Svelte. If you don’t, make sure you have a quick read from the article below for introduction, and you can continue here, where you left off.

Looking into Svelte 3

The Structure of Sapper

To start working with Sapper, you can run the following commands from their official site, depending on whether you want to go with Rollup or Webpack:

npx degit "sveltejs/sapper-template#rollup" imdb-sapper
How to install Sapper

In this tutorial, I will be using Rollup. Once installed, you can run npm i to install the dependencies, and then run npm run dev to start your dev server. You’ll see that Sapper will create the following folder structure, from which three of them is important to us:

The folder structure of Sapper

The scripts folder serves only one purpose, to set up TypeScript. To integrate TypeScript into the project, run:

node scripts/setupTypeScript.js

The src folder contains the entry point for your application. You will see three important files at the root of the folder:

client.js:

The entry point for the client-side of the application. This must call the start function from your Sapper module, with a target property, referencing a DOM element, like so:

import * as sapper from '@sapper/app';

sapper.start({
    target: document.querySelector('#sapper')
});
client.js
Copied to clipboard!

Usually, you will have nothing more, except the above. Although you are free to add any configuration you may like. This code example references the #sapper div element, that is found in your template.html.

template.html:

<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width,initial-scale=1.0" />
        <meta name="theme-color" content="#333333" />

        %sapper.base%

        <link rel="stylesheet" href="global.css" />
        <link rel="manifest" href="manifest.json" crossorigin="use-credentials" />
        <link rel="icon" type="image/png" href="favicon.png" />

        %sapper.scripts%
        %sapper.styles%
        %sapper.head%
    </head>
    <body>
        <div id="sapper">%sapper.html%</div>
    </body>
</html>
template.html
Copied to clipboard!

You will notice that this file uses some special placeholders, where Sapper will inject the necessary content. In order they are:

All of this will be served from your server.js file, which uses polka:

server.js:

import sirv from 'sirv';
import polka from 'polka';
import compression from 'compression';
import * as sapper from '@sapper/server';

const { PORT, NODE_ENV } = process.env;
const dev = NODE_ENV === 'development';

polka()
    .use(
        compression({ threshold: 0 }),
	sirv('static', { dev }),
	sapper.middleware()
    ).listen(PORT, err => {
        if (err) console.log('error', err);
    });
server.js
Copied to clipboard!

Polka is a lightweight alternative to Express. Of course, you are free to use anything else, if you want to change the behavior of your server. Again, if you want to change your base URL, you can add it as the first parameter inside the use function:

...

polka().use(
    '/base-url',
    compression({ threshold: 0 }),
    ...
)
server.js
Copied to clipboard!

Apart from these core files, you will also see three folders: components, node_modules, and routes. The first question is: Why is there a node_modules folder inside the source folder? And the answer is that Sapper generates a package for your app before bundling, that you can access by importing from @sapper/app. Putting this package inside the source folder means it won’t be deleted if you add new dependencies. You can access these four core function from Sapper:

The Client API that Sapper provides
Click on the image to navigate to the docs.

You also have the usual components folder for storing components, and a routes folder for the different routes your application will have. This is where we will mostly work.

routes

Inside your routes folder, you will see a blog folder with a couple of svelte files next to it. The way you name your components will correspond to a route. For example, index.svelte will be used for the root path, while about.svelte will be used for /about, and so on. You can also organize these routes into folders. For example, the same route will be served for the following:

about.svelte        -> /about
about/index.svelte  -> /about

search.svelte       -> /search
search/index.svelte -> /search
Copied to clipboard!

You will also see files with an underscore in front of their names. In Sapper, these files will not create routes. Instead, you can use them to create modules or components for the routes that depend on them. With the starter code, there are two special files in the routes folder. These are:

And lastly, you will also have a static folder, for holding static assets, such as your icon or fonts. That’s is all you need to know about the structure of your Sapper app. Enough talk, let’s get into coding stuff.


Setting Up the List View

You can get rid of almost anything from your src folder, only keep the folder structure. The index.svelte will be the list view. We want to request the movies here with a special preload function:

<script context="module">
    export async function preload() {
        const res = await this.fetch('movies.json');
        const data = await res.json();

        return {
            movies: data
        };
    }
</script>

<script>
    export let movies;
</script>
index.svelte
Copied to clipboard!

This function runs before a component is created. It is called both on the client and the server-side as well, so you want to avoid referencing any browser API inside this function. Make sure you set the context to module on your script tag, as this runs before the component is created. To fetch data, you want to use this.fetch inside the function.

You can expose the return value to your component through an export. So where is this data coming from? It references movies.json. To make this endpoint available to Sapper, you want to create a movies.json.js file next to your index.svelte.

Fetching data

This file needs to export a get function, where you can send your JSON response. For now, this is all it does:

import movies from '../data/movies';

const movieList = movies.map(movie => ({
    title: movie.title,
    slug: movie.slug,
    image: movie.image,
    score: movie.score
}));

export function get(request, response) {
    response.writeHead(200, {
        'Content-Type': 'application/json'
    });

    response.end(JSON.stringify(movieList));
}
movies.json.js
Copied to clipboard!

This is where you would fetch data from your database, but to keep things simple for this tutorial, it uses mock data that is imported from a movies.js file in a data folder.

export default [
    {
        title: 'Narcos',
        slug: 'narcos',
        image: 'src-path',
        score: 8.8,
        description: 'A short description',
        creators: 'The creators',
        stars: 'The stars'
    },
    { ... },
    { ... }
];
movies.jsThe structure of each entry
Copied to clipboard!

Since we are only interested in part of the data, we can use a map to get rid of the unnecessary stuff. Then this data gets stringified and sent down to the client as JSON. If you go over to your localhost and hit /movies.json, you should be able to see the data returned from the server.

The page response sent from Sapper

To display a card for each entry, you want to create an #each loop in your component:

<script>
    import Card from '../components/Card.svelte';
</script>

<svelte:head>
    <title>Top picks</title>
</svelte:head>

<ul>
    {#each movies as movie}
        <li>
            <a rel="prefetch" href="movie/{movie.slug}">
                <Card
                    title={movie.title}
                    image={movie.image}
                    score={movie.score} />
            </a>
        </li>
    {/each}
</ul>
index.svelte
Copied to clipboard!

Note that the anchor uses rel="prefetch". This tells Sapper to load the necessary data for the URL as soon as the user hovers over it.

data is prefetched as soon as the user hovers over a card

Adding Sass support

To style the cards I’m using Sass to enhance the readability of the rules by nesting them. To add Sass support, you want to npm i svelte-preprocess-ass. To add it to your bundler, open your rollip.config.js, and inside your svelte plugin call, add this object both for the client and the server as well:

// Import the preprocessor
import { sass } from 'svelte-preprocess-sass';

export default {
    client: {
        svelte({
            ...
            preprocess: {
                style: sass()
            }
        })
    },
    server: {
        svelte({
            ...
            preprocess: {
                style: sass()
            }
        })
    }
}
rollup.config.js
Copied to clipboard!

Now inside your components, you can define a type for the style tags, and that way, you can use Sass:

<style type="text/scss">

</style>
index.svelteAll CSS is included in the GitHub repository
Copied to clipboard!

The Card component

Inside the link, we have a Card component, that I’ve created inside the components folder. This way, we can later reuse this layout. It accepts the properties of a movie. Create the component and add the following:

<script>
    import Rating from './Rating.svelte';

    export let image;
    export let score;
    export let title;
</script>

<style type="text/scss">
    ...
</style>

<article>
    <img src={image} alt={`${title} poster`} />
    <div>
        <Rating score={score} />
        <h3>{title}</h3>
    </div>
</article>
Card.svelte
Copied to clipboard!

Once again, I’ve created a component for the rating, that is made up of a star icon and a score. It may seem overkill for this small application, but if you were to build a full clone of IMDB, you want to keep the UX consistent across different pages. And one way to achieve this is to use common components. It also lets you maintain your app more easily. As mentioned, it is only made up of an SVG and a score, with a couple of styles:

<script>
    export let score
</script>

<style type="text/scss">
   div {
      ...

        svg {
            ...
        }

        span {
            ...
        }
    }
</style>

<div>
    <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg" class="ipc-icon ipc-icon--star-inline" viewBox="0 0 24 24" fill="currentColor" role="presentation"><path d="M12 20.1l5.82 3.682c1.066.675 2.37-.322 2.09-1.584l-1.543-6.926 5.146-4.667c.94-.85.435-2.465-.799-2.567l-6.773-.602L13.29.89a1.38 1.38 0 0 0-2.581 0l-2.65 6.53-6.774.602C.052 8.126-.453 9.74.486 10.59l5.147 4.666-1.542 6.926c-.28 1.262 1.023 2.26 2.09 1.585L12 20.099z"></path></svg>
    <span>{score}</span>
</div>
Rating.svelte
Copied to clipboard!

Note that since Svelte automatically scopes CSS, we can simply use tag selectors. And with that, you should have a list of top picks displayed for you to choose from.

List of movies created with Sapper

Setting Up the Details View

Let’s turn our attention to the details of the movie. You may noticed in index.svelte that the href of the link is pointing to:

"movie/{movie.slug}"

This means we will need a movie folder inside the routes, and to create a dynamic route, we want to use regex in the file name. If we name our file as [slug].svelte, a parameter called slug will be available in our preload function:

<script context="module">
    export async function preload({ params }) {
        const result = await this.fetch(`movie/${params.slug}.json`);
        const data = await result.json();
        
        return result.status === 200
            ? { movie: data }
            : this.error(result.status, data.message);
    }
</script>
[slug].sveltethis.error lets you render an error page if a request is failed
Copied to clipboard!

You can see, this time we want to fetch the JSON data from movie/[slug].json. The params object will be populated based on the URL we want to request:

/movie/narcos    -> params.slug === 'narcos'
/movie/westworld -> params.slug === 'westworld'
Copied to clipboard!

To create the necessary JSON endpoints, we will need to create a [slug].json.js file next to our component. Just like for all movies, we will need to use the get function to fetch a single movie. To get the one we are looking for, we can use Array.find:

import movies from '../../data/movies';

export function get(request, response, next) {
    const { slug } = request.params;
    const movie = movies.find(movie => movie.slug === slug);

    if (movie) {
        response.writeHead(200, {
	   'Content-Type': 'application/json'
	});

	response.end(JSON.stringify(movie));
    } else {
        response.writeHead(404, {
	    'Content-Type': 'application/json'
	});

	response.end(JSON.stringify({
           message: 'Movie not found',
	}));
    }
}
[slug].json.jsIn case the movie is not found, we can return a 404, with an error message.
Copied to clipboard!

To display the additional information, let’s go back to [slug].svelte, and add the layout. Here we can reuse the Card component we’ve created earlier, with an additional param:

<script>
    import Card from '../../components/Card.svelte';

    export let movie;
</script>

<svelte:head>
    <title>{movie.title}</title>
</svelte:head>

<main>
    <Card
        showBackButton={true}
        title={movie.title}
        image={movie.image}
        score={movie.score} />
</main>
[slug].svelte
Copied to clipboard!

We have a showBackButton prop, that we didn’t have before, so let’s enhance the Card component with that. Open your Card.svelte file and extend the component with the following code:

<script>
    import Rating from './Rating.svelte';
    import BackButton from './BackButton.svelte';

    export let image;
    export let score;
    export let title;
    export let showBackButton;
</script>

<article>
    {#if showBackButton}
        <BackButton />
    {/if}
    <img src={image} alt={`${title} poster`} />
    <div>
        <Rating score={score} />
        <h3>{title}</h3>
    </div>
</article>
Card.svelte
Copied to clipboard!

This will add a BackButton to the card, given that we have the flag set to true. And again, the back button only includes an SVG with an anchor that points back to the top picks page:

<a href="/">
    <svg xmlns="http://www.w3.org/2000/svg" width="31" height="26" viewBox="0 0 31 26" fill="none">
        <path fill-rule="evenodd" clip-rule="evenodd" d="M11.5948 24.6485L0.988154 14.0419C0.337283 13.391 0.337283 12.3357 0.988154 11.6848L11.5948 1.07824C12.2456 0.427371 13.3009 0.427371 13.9518 1.07824C14.6027 1.72912 14.6027 2.78439 13.9518 3.43527L6.19036 11.1967L28.8333 11.1967C29.7538 11.1967 30.5 11.9429 30.5 12.8634C30.5 13.7838 29.7538 14.53 28.8333 14.53L6.19036 14.53L13.9518 22.2914C14.6027 22.9423 14.6027 23.9976 13.9518 24.6485C13.3009 25.2993 12.2456 25.2993 11.5948 24.6485Z" fill="#F5C518"/>
    </svg>
</a>
BackButton.svelte
Copied to clipboard!

Now if you click on one of your cards, you will get redirected to a details page. The only difference so far, is that you have a back button, and a single card in the middle of the screen, so let’s go ahead and add the rest of the properties. Remember, inside movies.js, we also had a description, a creators and stars node, so add them below your Card inside [slug].svelte:

<main>
    <Card
        showBackButton={true}
        title={movie.title}
        image={movie.image}
        score={movie.score} />
    <div>
        {movie.description}

        <div class="list">
            <b>Creators:</b> {movie.creators}
        </div>

        <div class="list">
            <b>Stars:</b> {movie.stars}
        </div>
    </div>
</main>
[slug].svelte
Copied to clipboard!

And now you should see every detail about each movie, as you navigate between them.

Navigating between routes in Sapper

Showing the Error Page

So what happens if we try to hit a page that doesn’t exist? We want to indicate somehow to the user that the page they are looking for doesn’t exist. Sapper has a special _error.svelte page just for this purpose. It should be located at the root of your routes folder. to display a 404 message, add the following code to the file:

<script>
    import errorGif from 'images/error.gif';

    export let status;
    export let error;

    const dev = process.env.NODE_ENV === 'development';
</script>

<svelte:head>
    <title>{status} - Movie not found</title>
</svelte:head>

<main>
    <h1>{status}</h1>
    <p>{error.message}</p>

    {#if dev && error.stack}
        <pre>{error.stack}</pre>
    {/if}

    <img src={errorGif} alt="error" />
</main>
_error.svelte
Copied to clipboard!

A couple of things to note; We only want to print the stack trace if we view the app through a dev server. We don’t want it to show in production. Secondly, an image is imported from images/error.gif. This is coming from:

src/node_modules/images/error.gif

Also, note that we have two props: status and error. These are coming from a this.error call from the movie directory. Essentially, we’ve set the message for the error inside [slug].json.js. If you try to hit an invalid page, you should get the following rendered:

Error page rendered by Sapper

Summary

And now you have the foundation to build apps in Sapper. So what are the next steps? All that’s left to do now is to build and deploy your app by running npm run build.

And what about the future of Sapper? The team is currently working on a successor of Sapper, called SvelteKit. It’s still in development, and things will likely change, but migration from Sapper to SvelteKit should be relatively simple. This also means Sapper will not reach v1.0, and once SvelteKit is finalized, you should adopt it, rather than starting a project in Sapper. For now, since SvelteKit is still early in development, it is recommended to start new projects with Sapper. If you’re interested in the future of Svelte and Sapper, make sure to check out the video below.

If you would like to play around with the whole project in one piece, you can clone it from GitHub. Have you used Sapper before? Let us know your thoughts in the comments below! Thank you for reading through, happy coding!

How to Make Your Very First Desktop App With Electron and Svelte

📚 Get access to exclusive content

Want to get access to exclusive content? Support webtips to get access to tips, checklists, cheatsheets, and much more. ☕

Get access Support us
Read more on
🎉 Thank you for subscribing to our newsletter. x