| API endpoints in SvelteKit
An introduction to setting up API endpoints with SvelteKit.
Sep

Intro

Note: This tutorial follows, in part, the code which can be found at this repo. I’d recommend cloning this and following the setup instructions to see things in action first.


Over the past few years I’ve come to enjoy working with Svelte, and in particular SvelteKit. It reduced a lot of the friction I had experienced in the first years of the single-page app (SPA) frenzy, when frameworks like React and Vue were just coming into their own. Since that time there have been numerous frontend frameworks, and while each has their merits, I’ve stuck with Svelte exclusively since the 2019 release of version 3.

In this post I’m going to go over setting up a toy API in SvelteKit backed by a file-based database. We’ll be using Prisma for the database, mostly because of its ease of use in getting set up. This could, alternatively, be written using an array shared between API routes, or something equally simple.

Project setup

If you’d like to follow along and build the project from scratch continue with the directions below, otherwise, clone the repo and run the application by following the directions provided.

To develop the project from scratch, the first thing we’ll do is use the standard SvelteKit project creation found on their site. It will ask you for options during this process and you should select Skeleton project and Typescript syntax and any additional options that you think are necessary.

1
2
3
4
npm create svelte@latest api-demo
cd api-demo
npm install
npm run dev -- --open

You can omit the – –open if you don’t want it to automatically open the web browser and display your current project.

Your base folder structure will look like this:

1
2
3
4
├── src
│   ├── lib
│   └── routes
└── static

You’ll add the API and Prisma folder as shown:

1
2
3
4
5
├── src
│   ├── api
│   ├── lib
│   └── routes
└── static

If you’ve looked at my repo, you’ll notice that this structure is different, and that’s because I’ve tried to isolate each section. The truth is, your api/ folder can go anywhere, though more often than not you’ll find it in the base of src/.

Setting up the database

In order to set up our database we need to install Prisma, which can be done from your projects root folder using:

1
2
npm install prisma
npx prisma init

This will create a prisma/ folder with a file called schema.prisma which you’ll want to edit to look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:dev.db"
}

model User {
  id Int @default(autoincrement()) @id
  username String
  email String
}

What this is doing is saying we want to create file-base SQLite database which will be saved in a file named dev.db. This file will reside in our prisma/ folder. In this database we will have a single table which will contain user information as shown.

The nice thing about Prisma, at least for Typescript development, is that it generates types for our tables which we’ll use later. To get everything with Prisma going, we need to do the following from our project’s root folder:

1
2
npx prisma generate
npx prisma db push

This will generate the types, as well as our Prisma client, and then push the changes. Think of this push as finalizing the changes you’ve made. There are also migration tools you can use, but we don’t need them for this project.

Creating a POST route

For each of the routes we will create we’ll create two folders with the same name, with one going in routes/ and then other in api/. The routes folders will container .svelte files viewed on the frontend by our user, while the API folders will contain .ts files, and be run on the server.

The first route we’ll create is called post, so we’ll add a post/ folder inside routes/, and a corresponding post/ folder in api/. Keeping the names the same is not necessary, but for our case it makes things more explicit. The route file is a user-facing route page, so it’ll be named +page.svelte and the API file will be named +server.ts.

The complete src/ folder will look like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
src/
├── api
│   └── post
│       └── +server.ts
├── app.d.ts
├── app.html
├── lib
│   └── index.ts
└── routes
   ├── +page.svelte
   └── post
       └── +page.svelte

If you haven’t encountered SvelteKit before, it’ll seem weird to add a “+” in front of filenames, and while it’s something that I’m still not a fan of, I’ve gotten used to it. Personally, I’d rather have it default to files named the same as the folder they’re in such as post/Post.svelte or something similar.

Let’s first look at the post/+page.svelte file, and create what our user will see when they want to add a new user to the database. You can either copy the code from my repo, which you’ll find in the routes/api-endpoints/post folder, or you can follow the simplified version I’ve created below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script lang="ts">
  let username: string = "";
  let email: string = "";

  async function post() {
    const response = await fetch("/api/post", {
      method: "POST",
      body: JSON.stringify({
        username: username,
        email: email,
      }),
      headers: {
        "content-type": "application/json",
      },
    });

    let res = await response.json();
  }
</script>

<div>
  <input type="input" id="username" placeholder="Username" bind:value={username} />
  <input type="input" id="email" placeholder="email" bind:value={email} />
  <button type="button" id="submit" on:click={post}>Add</button>
</div>

As with all Svelte files this is broken into the section and the HTML below. We have a function called post which is bound to the button’s on:click, and variables username and email bound to their respective variables.

The post function is fairly straightforward, making a fetch call to our API which uses the POST method, and our API route which matches the folder structure of our project. These functions should be async, as fetch returns a promise.

Now, let’s look at the endpoint for this, meaning the api/post/+server.ts file we created, which is currently empty. Again, you can copy this file from the repo or from below:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import type { User } from "@prisma/client";
import type { RequestHandler } from "./$types";
import { prisma } from "$lib/db-client";

export const POST: RequestHandler = async ({ request }) => {
  let user: User = await request.json();
  let result = await prisma.user.create({
    data: {
      username: user.username,
      email: user.email,
    },
    select: {
      username: true,
    },
  });

  if ("username" in result) {
    return new Response(JSON.stringify({ status: "success" }));
  }
  return new Response(JSON.stringify({ status: "error" }));
};

One pasted in you may find there are some errors with your code. The first set of errors might be type errors. If you haven’t run your app after adding these files it won’t have generated the new types. Run npm run dev and it should fix itself.

The other error will come from the the line:

1
import { prisma } from "$lib/db-client";

This line is creating our Prisma client, which we’ll put in a file called db-client.ts in our lib/ folder. In that file put the following:

1
2
import { PrismaClient } from "@prisma/client";
export const prisma = new PrismaClient();

Doing this, Prisma will create and manage our database pool for us, unless we explicitly want to define the number of connections in the pool. When you import the client into other files and use it, a connection from that pool will be used. This will save us time by reusing opened connections.

What you want to avoid is rewriting the above code in every API route, as then you’re creating a new connection each time you call the API instead of allowing the pool to hold open connections which are reused. By not reusing the connections you’ll take a performance hit, though the program will still work.

Getting back to our API we have our file +server.ts (or .js). All +server files are run on the server only. This means SvelteKit is taking care of everything necessary to wire it up on the server-side to receive messages from the frontend. In this case we’re receiving POST messages as denoted by the exported variable.

We then take the following steps:

  • Parse the request body as a User type which corresponds to the generated type by Prisma
  • Create the record in our database
  • Check if it was created correctly and return a response

Note, that a better way of responding would be to use the appropriate HTTP error codes.

On the user’s side in our post/+page.svelte file we receive the result and parse it:

1
let res = await response.json();

If you print this out to the console you should see that it has returned a success message in JSON form.

Creating a GET route

Now that we can add items to the database we’ll want to also retrieve items. The code this, and the other routes demo’ed in the repo are done in a similar way to the previous one, just with a few small changes. First, let’s add folders and files so that you have the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
src/
├── api
│   └── get-all
│       └── +server.ts
├── app.d.ts
├── app.html
├── lib
│   └── index.ts
└── routes
   ├── +page.svelte
   └── get-all
       └── +page.svelte

And in the +page.svelte file we’ll add:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<script lang="ts">
  import { onMount } from "svelte";
  import type { User } from "@prisma/client";

  let users: User[] = [];

  async function getUsers() {
    let getResponse = await fetch("/api/get-all", {
      method: "get",
    });
    users = await getResponse.json();
  }

  onMount(async () => {
    updateUsers();
  });
</script>

<p>This route will return all users currently in the database.</p>
<h3 class="program-output">Example program output</h3>
<hr />
<div class="table-container">
  <table>
    <tr>
      <th>ID</th>
      <th>Username</th>
      <th>Email</th>
    </tr>
    {#each users as user}
      <tr>
        <td>{user.id}</td>
        <td>{user.username}</td>
        <td>{user.email}</td>
      </tr>
    {/each}
  </table>
</div>

The server side of things is rather simple this time:

1
2
3
4
5
6
7
import type { RequestHandler } from "./$types";
import { prisma } from "$lib/db-client";

export const GET: RequestHandler = async ({ url }) => {
  const results = await prisma.user.findMany({});
  return new Response(JSON.stringify(results));
};

You’ll notice that we’re using GET instead of POST, and that since we’re getting all records, the code is simpler. Once you’ve added this you should be able to navigate to the route /get-all and see any of the names you’ve added from your /post route.

And that’s really all you need to add in a GET route. Of course, there are many things we’ve glossed over here in order to aid readability, such as more robust error handling and checking whether the user is authorized to access that particular endpoint or not.

Adding more routes

Adding more API routes is as simple as adding another folder and +server.ts file, then letting SvelteKit wire everything up for you. You can check out the demo in the repo for more examples of how to do routes, including deleting, updating and pagination.