SvelteKit Endpoints

Table of Contents

Introduction

⚠️ The post is outdated since the breaking SvelteKit changes but I’m going to update the post once things are more stable.

In this post I’m going to explain what are SvelteKit endpoints and when to use standalone endpoints and page endpoints or both.

If you want to try it out yourself you can find the repository on GitHub or open it on StackBlitz requiring no setup.

I’m going to mostly talk about SvelteKit endpoints but if you just started learning SvelteKit I have a SvelteKit For Beginners series and if you’re curious I also wrote Learn How SvelteKit Works.

What Are Endpoints?

An endpoint is a way to interact with another machine on the internet that returns a specific HTTP response to a HTTP request.

Let’s say for example you wanted some placeholder images in which case you could use JSONPlaceholder that provides fake data using a REST API.

The url looks like https://jsonplaceholder.typicode.com/photos and it returns a JSON response of key-value pairs.

Using a backend framework like Express you can create an API endpoint where you can get data from anywhere and have complete control over what you return.

example.js
import express from 'express'

const app = express()
const port = 3000

app.get('/photos', async (req, res) => {
	const response = await fetch(
		'https://jsonplaceholder.typicode.com/photos'
	)
	const photos = await response.json()
	res.json(photos.slice(0, 100))
})

app.listen(port)

The endpoint returns a set amount of results but you can let the user pass the amount such as /photos?amount=10 extending the functionality of the original API but also shape the data anyhow you want.

🐿️ If you’re using an older version of Node that doesn’t support the Fetch API you have to install the node-fetch package.

If you visit api.example.com/photos you can see the JSON response and now you have an API endpoint you can consume on the frontend to display some placeholder images.

SvelteKit Endpoints

Instead of having a separate backend and frontend SvelteKit combines them together into one cute package.

Instead of specifying a /photos endpoint like in the Express example in SvelteKit you just create a photos.json.ts file in your routes folder that becomes available at example.com/photos.

Endpoints are modules written in .js (or .ts) files that export request handler functions corresponding to HTTP methods.

This has multiple benefits from being able to talk to a database or reading from a file system because it runs on the backend which wouldn’t be possible on the frontend.

SvelteKit endpoints work by exporting request handler functions that correspond to HTTP request methods.

example.js
export function POST(event) {...}
export function PUT(event) {...}
export function PATCH(event) {...}
export function DELETE(event) {...}

The job of these functions is to return a response that includes the status, headers and body of the response.

Using a modern web framework like SvelteKit you knowledge is transferable because you’re going to spend more time learning web fundamentals on MDN than learning framework specific abstractions.

Standalone Endpoints

In SvelteKit you can create a standalone endpoint like in the Express example that you can expose to the world or consume it yourself.

Standalone endpoints can have an optional file extension like photos/index.json.ts making it available at example.com/photos.json — it could also be other things such as XML or images and it sets the proper response headers where using photos/index.ts is directly available at example.com/photos.

Let’s look at the same Express example in SvelteKit.

photos/index.json.ts
import type { RequestHandler } from '@sveltejs/kit'

export const GET: RequestHandler = async () => {
	const response = await fetch(
		'https://jsonplaceholder.typicode.com/photos'
	)
	const photos = await response.json()

	return {
		body: {
			photos: photos.slice(0, 100)
		}
	}
}

Anyone can use the API at example.com/photos.json if that’s what you want but we can also consume it on the frontend using the load function that runs before the page is created and returns the page props.

photos/index.svelte
<script context="module" lang="ts">
	import type { Load } from '@sveltejs/kit'

	export const load: Load = async ({ fetch }) => {
		const response = await fetch('/photos.json')
		const { photos } = await response.json()

		return {
			props: { photos }
		}
	}
</script>

<script lang="ts">
	import type { Photo } from '$lib/types'

	export let photos: Photo[]
</script>

<h1>Photos</h1>

<div class="grid">
	{#each photos as photo}
		<a sveltekit:prefetch href="/photos/{photo.id}">
			<img src={photo.thumbnailUrl} alt={photo.title} />
		</a>
	{/each}
</div>

<style>
	.grid {
		display: grid;
		grid-template-columns: repeat(4, 1fr);
		gap: 0.4rem;
	}
</style>

If you visit the /photos route at example.com/photos you should see a list of placeholder images.

The types come from lib/types/index.ts but you can ignore the types if you don’t care about TypeScript.

lib/types/index.ts
export type Photo = {
	albumId: number
	id: number
	title: string
	url: string
	thumbnailUrl: string
}

Using load to fetch data from an external API is great but we can’t talk to the database or read from the file system because it runs on the server and client and it’s pure boilerplate if you already wrote the same code inside a standalone endpoint.

Page Endpoints

In the previous example we created a standalone endpoint that fetches some placeholder images and shows them when you visit the photos page.

If your goal isn’t to create a public facing API to be consumed by multiple pages and the only thing that cares about the data is your page then you should use a page endpoint.

Sveltekit endpoints guide

The only change you have to do is rename the index.json.ts to index.ts to convert a standalone endpoint to a page endpoint and remove the redundant boilerplate code from index.svelte.

photos/index.ts
import type { RequestHandler } from '@sveltejs/kit'

export const GET: RequestHandler = async () => {
	const response = await fetch('https://jsonplaceholder.typicode.com/photos')
	const photos = await response.json()

	return {
		body: {
			photos: photos.slice(0, 100)
		}
	}
}

Anything you return from the page endpoint inside body is going to get passed as props to the page.

photos/index.svelte
<script lang="ts">
	import type { Photo } from '$lib/types'

	export let photos: Photo[]
</script>

<h1>Photos</h1>

<div class="grid">
	{#each photos as photo}
		<a sveltekit:prefetch href="/photos/{photo.id}">
			<img src={photo.thumbnailUrl} alt={photo.title} />
		</a>
	{/each}
</div>

<style>
	.grid {
		display: grid;
		grid-template-columns: repeat(4, 1fr);
		gap: 0.4rem;
	}
</style>

This is awesome! 😄

Page endpoints are powerful because they enable progressive enhancement for forms because a form has it’s own endpoint among other things.

The action attribute isn’t required on the form because it shares the same endpoint.

example.svelte
import { enhanceForm } from '$lib/actions/form'

<form method="post" use:enhanceForm>
	<input type="text" name="user" />
	<button type="submit">Submit</button>
</form>
example.ts
export const POST: RequestHandler = async ({ request }) => {
	const form = await request.formData()
	const user = form.get('user')
	return {}
}

If JavaScript is available on the page the form action is going to be used but if JavaScript fails for whatever reason it’s going to work like a regular form.

If you want to learn how to use progressive enhancement you can look at the default example when you initialize a new SvelteKit project.

Cases For Using Them Together

You learned when to use a standalone endpoint and a page endpoint but sometimes you want to use both.

I want to be able to show a single photo.

photos/[id].ts
import type { RequestHandler } from './__types/[id]'

export const GET: RequestHandler = async ({ params }) => {
	const response = await fetch(
		`https://jsonplaceholder.typicode.com/photos/${params.id}`
	)
	const photo = await response.json()

	return {
		body: { photo }
	}
}
photos/[id].svelte
<script lang="ts">
	import type { Photo } from '$lib/types'

	export let photo: Photo
</script>

<h1>{photo.title}</h1>
<img src={photo.url} alt={photo.title} />

If you visit example.com/photos/1 it works great but what if you wanted to navigate to the page only after the image is loaded to prevent flickering?

You can pass the props from the page endpoint to the load function and load the image before navigating to the page — pretend loadImage is some function imported from utils.

photos/[id].svelte
<script context="module" lang="ts">
	import { browser } from '$app/env'
	import type { Load } from '@sveltejs/kit'

	// utils/loadImage.ts
	async function loadImage(src: string) {
		await new Promise((resolve, reject) => {
			const img = new Image()
			img.onload = () => resolve('Loaded')
			img.onerror = () => reject(new Error('Failed to load image'))
			img.src = src
		})
	}

	export const load: Load = async ({ props }) => {
		if (browser) {
			await loadImage(props.photo.url)
		}

		return { props }
	}
</script>

<script lang="ts">
	import type { Photo } from '$lib/types'

	export let photo: Photo
</script>

<h1>{photo.title}</h1>
<img src={photo.url} alt={photo.title} />

Thanks to sveltekit:prefetch when you hover over or press the image link the image starts to download and is already loaded before you navigate if you look at the network tab.

Network tab showing the image prefetching

This solves the flickering because the image wasn’t loaded yet.

Another example is setting Cache-Control HTTP headers for the page because you have to set it on the HTML document and not the page endpoint to be cached on a content-delivery network.

This only caches the data.

example.ts
import type { RequestHandler } from '@sveltejs/kit'

export const GET: RequestHandler = async ({ params }) => {
	return {
		status: 200,
		body: { data: [1, 2, 3, 4] },
		headers: {
      'Cache-Control': 's-maxage=60',
		},
	}
}

This is going to cache the HTML document.

example.svelte
<script context="module" lang="ts">
  import type { Load } from '@sveltejs/kit'

  export const load: Load = ({ props }) => {
    return {
      props,
      cache: { maxage: 60 },
    }
  }
</script>

I use this method for the page you’re reading! I encourage you to open the network tab to see it in action.

Hope you learned about the difference between standalone endpoints and page endpoints and know when to use each one or both if you need it.

Thanks for reading! 🏄️

Found a mistake?

Every post is a Markdown file so contributing is simple as following the link below and pressing the pencil icon inside GitHub to edit it.

Edit on GitHub
Subscribe For Updates