Part 7: Connect frontend and backend
So far the frontend and the backend aren't connected. The
frontend in frontend/ still runs on its mock backend, and the FastAPI backend
in backend/ runs on its own at http://localhost:8000.
In this part we connect them, using the make commands we set up in
Part 6: A Makefile for the project.
Switch the frontend to the real backend
Back in Part 1: Frontend with Lovable, Lovable gave us two
service implementations behind services/index.ts: mock.ts and http.ts.
Everything so far has used the mock, and now we switch the entrypoint to the
real HTTP client.
Ask the assistant to do the swap in the service layer:
Switch the frontend to use the real backend client
The HTTP client calls the backend under a relative /api path. So the
browser sends every request to a single origin in local development, the Vite dev server
proxies /api to the backend on http://localhost:8000.
The Lovable template already configures this in frontend/vite.config.ts:
server: {
proxy: {
"/api": {
target: "http://localhost:8000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
}
With the proxy in place, the frontend at localhost:8080 calls /api/... on
its own origin. The dev server forwards it to FastAPI, so there's no
cross-origin request. If you instead point the client straight at
http://localhost:8000, the browser blocks it with a CORS error.
This is the most common first error when you connect a separate frontend and backend. You either keep this proxy or enable CORS on the backend. Tighten the CORS settings before going public, as Where to go from here explains.
Run both servers
The backend and frontend each run on their own, so start them with the two targets from the Makefile, one in each terminal.
Start the backend in one terminal:
make backend
Start the frontend in a second terminal:
make frontend
Now the React app at http://localhost:8080 calls FastAPI at
http://localhost:8000. Sign up for a real account, submit a score, and reload
the leaderboard - the data comes from the backend, not the mock.
Run both with one command
Two terminals and two commands work, but now that the frontend needs the backend running, it's easier to start both together.
Ask the assistant to add a dev target that does it:
Add a dev target to the Makefile that runs the backend and the frontend together
in one command, and stops both on Ctrl-C.
The assistant's first version puts all the startup logic into the Makefile target, which quickly becomes hard to read.
Ask it to move that logic into a script:
Move the dev startup logic into scripts/dev.sh and have the Makefile target
just call the script.
Now the target is one line:
dev:
@./scripts/dev.sh
And scripts/dev.sh starts both servers, remembers their process IDs, and
shuts both down on Ctrl-C:
#!/usr/bin/env bash
set -euo pipefail
backend_pid=""
frontend_pid=""
cleanup() {
trap - INT TERM EXIT
[[ -n "$backend_pid" ]] && kill "$backend_pid" 2>/dev/null || true
[[ -n "$frontend_pid" ]] && kill "$frontend_pid" 2>/dev/null || true
}
trap cleanup INT TERM EXIT
(cd backend && uv run python main.py) &
backend_pid=$!
(cd frontend && npm run dev) &
frontend_pid=$!
wait -n "$backend_pid" "$frontend_pid"
From now on one command starts the whole app:
make dev
Expect to debug here
In this step the two sides often disagree, and that's normal.
Common mismatches include:
- a field named one way in the frontend and another in the backend
- a 422 from a request body that doesn't match
- a missing trailing slash on a route
Read the error in the browser console or the uvicorn log, give it to the assistant, and ask for the fix.
Use openapi.yaml to decide which side is right, and change whatever differs from it.
Commit the integration:
git add .
git commit -m "Connect frontend to the backend API"
Every page now works against the real backend, but the data still lives only in memory and disappears on restart. We fix that with a real database in Part 8: Real persistence with SQLite.