How to Connect a React SPA to a FastAPI Backend
This is the shortest path to a React single-page app (SPA) calling a FastAPI backend with full types. At the end you’ll have FastAPI running on port 8000, a React app served by Vite 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 React project next to the backend:
npm create vite@latest frontend -- --template react-ts
cd frontend
npm install
npm install -D orval Vite gives you a single-page app out of the box. npm run dev serves it on port 5173, and npm run build turns it into static files. There’s no Node server in this setup; the only server is FastAPI.
Tell the frontend where the API is:
# frontend/.env
VITE_API_BASE_URL=http://localhost:8000 Vite exposes variables that start with VITE_ to the browser, and nothing else.
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
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
export const customFetch = async <T>(url: string, options: RequestInit): Promise<T> => {
const response = await fetch(`${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 component
// frontend/src/App.tsx
import { useEffect, useState } from 'react';
import { listItems } from './lib/api/gen/items';
import type { Item } from './lib/api/gen/model';
export default function App() {
const [items, setItems] = useState<Item[]>([]);
useEffect(() => {
listItems().then(setItems);
}, []);
return (
<ul>
{items.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
} Fetching in useEffect is fine for a page this small. Once the app has a router, load data in the route’s loader instead, so it’s ready before the page renders. The next post shows that.
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 App.tsx. 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 React 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 React. If you’d rather start from all of it already built, that’s FastReact.
