Back to Blog
Tutorial

Your First Full-Stack Feature: Wiring a Django REST API to React

SkyTrainings TeamEditorial Team
29 August 2026
5 min read

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.


From model to working endpoint
  1. 1

    Define the Model

    SavedCourse with user and course foreign keys, unique together

  2. 2

    Serialize It

    A ModelSerializer that returns course details, not just a raw ID

  3. 3

    Wire the ViewSet

    ModelViewSet requiring authentication, scoped to the requesting user's own rows

  4. 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.


Where a save request actually breaks
Loading diagram…

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


What actually changes between dev and prod
01

Allowed origins

localhost in dev; the real deployed frontend domain in prod, never a wildcard

02

Debug mode

On locally for readable tracebacks; off in prod, or Django leaks stack traces to visitors

03

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.

PythonDjangoReactFull StackTutorial