Your First Full-Stack Feature: Wiring a Django REST API to React
The Ticket: "Add a Save Button, Should Be Quick"
A product manager files it as a small ask: put a Save for Later button on each course card, so a logged-in visitor can bookmark a listing and come back to it without hunting through the whole catalog again. It sounds like an afternoon of work until you actually sit down to build it, because a two-word button touches nearly every layer of a full-stack app at once. There's a database table to hold the saved courses, an API endpoint that only a logged-in user can hit, and a frontend that updates the moment you click without waiting on a page reload.
This walkthrough builds that exact feature, start to finish, using the same Django REST Framework and React combination SkyTrainings' Python Full Stack course teaches. It assumes a Django project with a working User model and a React app already rendering a list of courses. The point here is wiring the two together, not starting from an empty folder.
Modeling the Save, Then Exposing It
The database side is the easy part. A SavedCourse model with a foreign key to the user, a foreign key to the course, and a unique-together constraint on the pair keeps someone from saving the same course twice.
- 1
Define the Model
SavedCourse with user and course foreign keys, unique together
- 2
Serialize It
A ModelSerializer that returns course details, not just a raw ID
- 3
Wire the ViewSet
ModelViewSet requiring authentication, scoped to the requesting user's own rows
- 4
Register the Route
One line in urls.py through DRF's router, no manual path per action
The permission layer is the part worth slowing down on. Requiring authentication stops an anonymous request, but the viewset's queryset method also has to filter to that user's own saved rows, or one logged-in user could page through someone else's saved list just by guessing IDs. That's not a hypothetical edge case; it's the single most common mistake in a first REST Framework viewset, and it stays invisible in local testing if you only ever test logged in as yourself.
The React Half: State That Moves Before the Server Answers
On the frontend, the button is a boolean piece of state and a click handler that posts to the new endpoint with the access token in the request header. The naive version waits for that request to resolve before the button's appearance changes, which feels sluggish even on a fast connection. Flipping the state immediately and rolling it back only if the request actually fails, an optimistic update, is what makes a save button feel instant instead of laggy.
Worth knowing if you're setting this project up today rather than off an older tutorial: skip Create React App. It's been effectively unmaintained for a while now, and running npm create vite@latest gets a working dev server up in under a minute, with none of the older tool's build overhead.
The Wall Almost Everyone Hits First: CORS
The model works in Django's admin. The endpoint returns a clean 200 in a REST client. Then the React app calls it, and the browser console shows an error nobody warned you about. This is the single most common stopping point in the whole exercise, and it's a browser security rule, not a bug in either half of the stack.
The fix is the django-cors-headers package, configured with the React dev server's actual origin, port 5173 if you're on Vite, added to the allowed-origins list. Turning that setting into a blanket allow-all makes the error disappear in development and is exactly the line that should never survive into a production deploy.
Local Settings Don't Survive Production Unchanged
Allowed origins
localhost in dev; the real deployed frontend domain in prod, never a wildcard
Debug mode
On locally for readable tracebacks; off in prod, or Django leaks stack traces to visitors
Allowed hosts
Empty or localhost in dev; the actual domain the app is served from in prod
Docker is where these settings actually get enforced rather than just documented. Reading them from environment variables instead of hardcoding them into the settings file means the same image runs correctly in both places, since only the values change between environments, not the code.
What Building This Actually Teaches
None of these pieces, a foreign key, a permission check, a CORS header, are individually hard. What takes real practice is noticing which layer a given failure lives in. A blank response means check the queryset. A browser console error means check CORS. A 401 means check the token, not the view code. That diagnostic instinct is what separates someone who has read about full-stack development from someone who can actually ship a feature end to end, and it only comes from building something real enough to break in these specific ways.
Build this project, save button, token auth, and Docker deploy included, inside SkyTrainings' Python Full Stack course.