How to Connect a SvelteKit SPA to a FastAPI Backend
This is the shortest path to a SvelteKit single-page app (SPA) calling a FastAPI backend with full types. At the end you’ll have FastAPI running on port 8000, a SvelteKit single-page app on port 5173, and a TypeScript client generated from the API so the frontend can’t call an endpoint that doesn’t exist.
No login, no database, no deploy. Just the two servers talking. Those come later.
The backend
You need Python 3.12+ and uv.
mkdir backend && cd backend
uv init
uv add "fastapi[standard]" Replace main.py with one endpoint and the CORS setup:
# backend/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
class Item(BaseModel):
id: int
name: str
ITEMS = [Item(id=1, name="First"), Item(id=2, name="Second")]
@app.get("/items", operation_id="list_items", tags=["items"])
def list_items() -> list[Item]:
return ITEMS Two things here you’d normally skip. The CORS block is required because the frontend runs on a different port, and the browser treats that as a different origin. And operation_id and tags decide what the generated TypeScript will be called: a function named listItems in a file named items.ts. Without them you get listItemsItemsGet in default.ts.
Run it:
uv run fastapi dev main.py Open http://localhost:8000/docs. You’ll see the endpoint. The file the frontend cares about is http://localhost:8000/openapi.json.
The frontend
Create a SvelteKit project next to the backend:
npx sv create frontend --template minimal --types ts
cd frontend
npm install -D @sveltejs/adapter-static orval Turn it into a single-page app. Two files:
// frontend/svelte.config.js
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: { adapter: adapter({ fallback: 'index.html' }) }
};
export default config; // frontend/src/routes/+layout.ts
export const ssr = false; ssr = false means SvelteKit renders everything in the browser. The fallback means the build produces one index.html that handles every route. There’s no Node server in this setup; the only server is FastAPI.
Tell the frontend where the API is:
# frontend/.env
PUBLIC_API_BASE_URL=http://localhost:8000 Generate the client
Orval reads the OpenAPI spec and writes typed functions. It needs a config and a small fetch wrapper.
// frontend/orval.config.ts
import { defineConfig } from 'orval';
export default defineConfig({
default: {
input: {
target: 'http://localhost:8000/openapi.json'
},
output: {
client: 'fetch',
target: './src/lib/api/gen',
schemas: './src/lib/api/gen/model',
mode: 'tags',
clean: true,
override: {
mutator: {
path: './src/lib/api/fetch.ts',
name: 'customFetch'
},
fetch: {
includeHttpResponseReturnType: false
}
}
}
}
}); // frontend/src/lib/api/fetch.ts
import { PUBLIC_API_BASE_URL } from '$env/static/public';
export const customFetch = async <T>(url: string, options: RequestInit): Promise<T> => {
const response = await fetch(`${PUBLIC_API_BASE_URL}${url}`, {
...options,
credentials: 'include'
});
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}`);
}
return response.status === 204 ? (undefined as T) : response.json();
}; Every generated function goes through customFetch. That’s the one place that knows the API address and sends cookies, which you’ll need the day you add login.
Add a script and run it, with the backend still running:
// frontend/package.json
"scripts": {
"generate": "orval"
} npm run generate You now have src/lib/api/gen/items.ts with a listItems() function and src/lib/api/gen/model/item.ts with the Item type. Don’t edit these files. Change the backend and regenerate.
Call it from a page
Load the data in the route’s load function, not in the component:
// frontend/src/routes/+page.ts
import { listItems } from '$lib/api/gen/items';
import type { PageLoad } from './$types';
export const load: PageLoad = async () => {
return { items: await listItems() };
}; <!-- frontend/src/routes/+page.svelte -->
<script lang="ts">
let { data } = $props();
</script>
<ul>
{#each data.items as item (item.id)}
<li>{item.name}</li>
{/each}
</ul> Start the frontend:
npm run dev Open http://localhost:5173. You’ll see the two items from the backend.
To check the types are real, rename name to title in the Python Item model, run npm run generate again, and look at +page.svelte. TypeScript now flags item.name. That’s the point of the whole setup: the backend and frontend can’t drift apart without you finding out at compile time.
What’s next
This runs on your laptop and that’s all it does. Real users need login, config that changes between your machine and the server, database migrations, and a deploy. Those are covered in How to Take a SvelteKit SPA + FastAPI App to Production.
For the reasoning behind this setup, and the choices you’ll face as it grows, read How to use FastAPI with Svelte. If you’d rather start from all of it already built, that’s FastSvelte.
