Building an app with Sveltekit
I built a full stack application using sveltkit on cloudflare, using some of the latest svelte and svelkit features like runes and remote functions.
It’s an image generation app that allows you to generate images using reference images, you can easily reuse images and prompts, it supports multiple different models.
Hosting It is hosted on cloudflare workers, you get nice apis to access cloudflares database D1 and storage buckets R2.
Bootstrapping Svelte has its own cli ‘sv’ to quickly bootstrap a new application, and select some additional tools to include like prettier, eslint, tailwind etc or you can use cloudflares cli to save yourself the cloudflare setup.
bunx sv create
or
bun create cloudflare@latest
Sveltkit Is a full stack framework for svelte, it will server side render pages initially then switch to client side rendering. It also lets you create api routes and handle http verbs.
src/routes/
+layout.svelte
+page.server.ts
+page.svelte
This is the root path /, layout is the parent template where you can add the navbar/footer things you want on all your pages. server.ts is code that will run on the server only.
Data loading To load data we can create a load function in the server file, that retrieves the initial data needed for rendering the view.
export const load: PageServerLoad = async ({ platform, depends, locals, url }) => {
depends('app:images');
const user = locals.user;
return {
images: getImages(platform!, user),
user,
showSuccessfulPayment: showSuccessfulPayment(url),
showError: showError(url),
}
}
This load function is called on server render and the resulting data available in the svelte template via props
let { data } = $props();
Props are typed here as well which is nice.
getImages returns a promise, not awaiting the promise is the load functions means the data is streamed to the client, making initial load faster.
Notice the depends call which sets a path we can use to invalidate the data. When we add or update data and need the load function to rerun we call invalidate passing the path
invalidate(‘app:images’)
This only reruns the load function but does not rerender the page.
To rerender the page you need to make your data observable.
To add more routes you can just add a folder and another page file
src/routes/about
+page.svelte
App state
Now you have some data in your frontend.
Svelte 5 introduced runes, which are reactive state, they still normal data types wrapped in a proxy.
So we want to render our image data we might have something like this:
<script>
const {data} = $props();
const images = $state(data.images)
</script>
{#each images as image}
<img src={image.url} />
{/each}
‘images’ is still an array, we can add to it, remove items and the ui will update as expected.
Classes
Svelte 5 lets you create an external “state” class and use runes, the file name should end in svelte.ts.
An instance of the class can be created and passed to child components via context.
I have created an AppState class to hold and manage app wide data, load images, load tags, getters for specific filtered views of data.
A class also allows you to add getter and setters and add functionality without having to use or rely on $effect and magic reactive features.
class AppState {
images = $state([])
get filteredImages() {
return this.images.filter(…
}
}
I try to keep most logic in a state class and component can have the minimal amount of code it needs, for pagination we keep the current page in state but the actual pagination functionality lives in the component.
Image results also have their own state class, an image result can have multiple images so we have functionality to iterate through the image collection making one selected.
In some cases you may still want to pass props to components rather than coupling them to your state class, making them simpler to test and easier to reuse.
Api routes
There is a form to submit and create a new image, we could use sveltkits built in form submission api:
+page.server.ts
import type { Actions } from './$types';
export const actions = {
generate: async (event) => {
// create image
},
default: async (event) => {..
} satisfies Actions;
In your server file to can export a default action to handle a form post, or multiple named actions, then handle the ‘form’ object returned to the client. I don’t like this api so instead I created an api route. Remote functions can handle forms but are still experimental and I used them for query other data.
To add an api route, you create a ‘+server.js’ file and export a function for any http verb:
export const POST: RequestHandler = ({ url, platform }) => {
The image generation endpoint lives in /routes/api/image/+server.js
Generating an image can take a long time so we return a stream, allowing us to push process updates to the client like this:
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const sendEvent = ({ event, data }: SSEEvent) => {
controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`));
};
…
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
}
});
Remote functions
Remote functions are still experimental in sveltkit 2.27 are need to be enabled in the svelte.config.js
They are basically just functions that allow you to call the server from the client in a type-safe manner.
import { query, command, getRequestEvent } from '$app/server';
import { getTagsForImage, getTagsForUser, assignTagToImage, removeTagFromImage } from '.';
import * as v from 'valibot';
export const getAllTagsQuery = query(async () => {
const {locals, platform} = getRequestEvent();
return getTagsForUser(platform!, locals.user);
})
export const addTagToImageCommand = command(v.object({imageId: v.string(), tagId: v.string()}), async ({imageId, tagId}: {imageId: string, tagId: string}) => {
const {platform} = getRequestEvent();
await assignTagToImage(platform!, imageId, tagId);
})
Query allows us to retreive data, and a command including validation supports mutations.
Svelkit provides handy functions to wrap our server side code in and handles all the over http stuff for us. getRequestEvent gives us access to our locals (user stuff) and platform which is our cloudflare bindings for access to the database and r2.
Here we are retrieving all the tags for the user, and in our svelte component
let tagsQuery = getAllTagsQuery();
</script>
{#if tagsQuery.error}
<p>oops!</p>
{:else if tagsQueryloading}
<p>loading...</p>
{:else}
<ul>
{#each tagsQuery.current as { title, slug }}
{/each}
</ul>
{/if}
We can handler loading and error states in our component and event refresh the data.
tagsQuery.refresh()
We call this when adding new tags, and in other scenarios using $effect when drop downs are opened.
Auth
Sveltkit has the notion of hooks, which run when certain events, the Handle event is called whenever the server receives a request, making it an ideal place to authenticate and secure endpoints.
in hooks.server.ts we can secure routes making sure we have a valid session cookie, and here is where we set the ‘locals’ that we use in api routes and remote functions to access the user.
export const handle: Handle = async ({ event, resolve }) => {
const isApi = event.url.pathname.startsWith('/api');
const isAuth = event.url.pathname.startsWith('/api/auth');
const isAuthLogout = event.url.pathname.startsWith('/api/auth/logout');
if (isUserHome || (isApi && !isAuth) || isAuthLogout) {
const session = event.cookies.get(cookieName);
if (!session) {
return redirect(302, loginPath);
}
const sessionSecret = event.platform!.env.SESSION_SECRET;
const user = await verifySessionToken(session, sessionSecret);
if (!user) {
event.cookies.delete(cookieName, { path: '/' });
return redirect(302, loginPath);
}
event.locals.user = {
email: user.email,
id: user.id,
};
}
const response = await resolve(event);
return response;
};
Cloudflare integration
If you have used cloudflare workers before you will know the concept of bindings, which allow you to use outher cloudflare services, to access these bindings you can get platform using the getRequestEvent in a remote function, or use the platform that is passed into an api route.
Any bindings or env vars you have added to the cloudflare config has types availble in ‘worker-configuration.ts’ which you can update via:
bun run cf-typegen
If your interested its imageeval