Part 7: Connect frontend and backend
So far the frontend and the backend have never spoken to each other. 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. To keep the
browser talking 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. That's the
classic first failure when a separate frontend and backend start talking. You either
keep this proxy or enable CORS on the backend, one of the things to
tighten before going public, as Where to go from here
calls out.
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 is talking to 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 handier 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 crams all the startup logic into the Makefile target, which gets noisy fast.
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 brings the whole app up:
make dev
Expect to debug here
This is the step where the two sides 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.
Let openapi.yaml settle the disagreement, and change whatever diverges 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.