Skip to main content

πŸ› οΈ Weekend Project: Python Web Frameworks

Two days, one real application. This weekend you'll ship BookTracker β€” a personal reading library with a Django REST API backend and a React frontend β€” by working through clear milestones instead of copy-pasting a tutorial. You'll finish with a portfolio-worthy full-stack app and a repeatable method for building the next one.

🎯 Learning Objectives

By the end of this project, you will be able to:

  • Apply Polya's four-step problem-solving method to a real software build (understand β†’ plan β†’ execute β†’ review)
  • Design and implement a Django REST Framework API with models, serializers, viewsets, and JWT authentication
  • Build a React frontend that consumes the API with an auth context, an axios client, and protected routes
  • Work to explicit milestones and a definition of done, so you know when each slice is finished
  • Review your own work against a "what good looks like" bar and plan sensible extensions

Estimated Time: One weekend (8–12 focused hours)  β€’  Difficulty: Intermediate

Hands-on: This entire lesson is the hands-on. Build the app milestone by milestone and tick the checklist as you go.

In This Lesson

The Project & The Method

You've spent this module learning Python web frameworks and how they connect to a React frontend. A weekend project is where that knowledge becomes muscle memory. Rather than following keystrokes blindly, you'll build BookTracker using a discipline that scales to any project: George Polya's four-step method from his 1945 classic How to Solve It.

πŸ’‘ Why a method, not just a tutorial? Tutorials teach you to reproduce one app. A method teaches you to build the next app β€” the one with no tutorial. Polya's four steps map almost perfectly onto professional software work, so practicing them here pays dividends for your whole career.
flowchart LR A["1 β€” Understand
the problem"] --> B["2 β€” Devise
a plan"] B --> C["3 β€” Execute
the plan"] C --> D["4 β€” Review
& extend"] D -.->|new features & fixes| A

πŸ“– What you're building: BookTracker

A personal library manager where a signed-in user can add books, move them through To Read β†’ Reading β†’ Completed, log reading sessions, jot notes, leave a review, filter their shelf, and see simple reading stats.

Notice that Polya's loop is a cycle, not a straight line. Real builds circle back: something you learn while executing sends you back to re-plan, and the review step feeds the next iteration. That's expected and healthy β€” this lesson is structured to make the loop visible.

The Milestone Map

Big projects fail when they're one giant undivided task. We'll slice the weekend into five milestones, each with a concrete definition of done. Aim to reach a working state at the end of each β€” never leave the app broken overnight.

MilestoneFocusDone when…Rough time
M0 β€” UnderstandRequirements & user storiesYou can state the app in one sentence and list its user stories30 min
M1 β€” PlanStack, data model, API, componentsYou have a schema, an endpoint list, and a component tree on paper1 hr
M2 β€” BackendDjango REST API + authEvery endpoint returns correct JSON, tested with curl/Postman3–4 hrs
M3 β€” FrontendReact UI + API integrationYou can register, log in, and do full CRUD on books in the browser3–4 hrs
M4 β€” ReviewTest, polish, extend, documentChecklist passes; README written; one extension attempted1–2 hrs

⚠️ Scope for a weekend, not a career

The "must-have" slice is auth + book CRUD + status filter. Reading sessions, notes, reviews, and statistics are "nice-to-have." Build the must-haves solidly first; treat everything else as a bonus. A small app that works beats a large app that doesn't.

Milestone 0 β€” Understand the Problem

Before a single line of code, get crisp on what and for whom. Cheap to change now, expensive to change later.

Ask the right questions

  • What problem? Readers lack a simple place to track what they own, what they're reading, and what they thought of it.
  • Who's the user? A single book enthusiast managing their own shelf β€” so every record is scoped to one owner.
  • What's core? Add/edit/delete books, track status, and keep it private per user.
  • What are the constraints? One weekend, Python + React, must work on a phone screen.
  • What does success look like? A logged-in user can manage their shelf end to end without errors.

Write user stories

User stories keep you honest about building features people asked for, in the form "As a <user>, I want <goal> so that <reason>."

1. As a reader, I want to register and log in so my shelf is private to me.
2. As a reader, I want to add a book so I can track what I own.
3. As a reader, I want to set a book's status (to-read / reading / completed)
   so I can see my progress at a glance.
4. As a reader, I want to filter my shelf by status so I can find books fast.
5. As a reader, I want to edit or delete a book so I can fix mistakes.
6. (Nice-to-have) As a reader, I want notes and a review per book
   so I remember my thoughts.
7. (Nice-to-have) As a reader, I want simple stats so I can see my habits.

Model the domain

Sketch how the "things" in the app relate. A user owns many books; each book can carry reading sessions, notes, and one review.

classDiagram User "1" --> "many" Book : owns Book "1" --> "many" ReadingSession : has Book "1" --> "many" Note : has Book "1" --> "1" Review : has class User { +username +email } class Book { +title +author +status +pages } class ReadingSession { +start_date +end_date +pages_read }

πŸ’‘ Milestone 0 β€” Definition of Done

You can describe BookTracker in one sentence, you've written your user stories, and you've marked which are must-have vs. nice-to-have. That's it β€” resist the urge to code yet.

Milestone 1 β€” Devise a Plan

With the problem understood, decide how you'll build it. Planning on paper for an hour saves many hours of thrashing in code.

The stack

LayerChoiceWhy
Backend frameworkDjango + Django REST FrameworkBatteries-included; serializers & viewsets remove boilerplate
AuthJWT (SimpleJWT)Stateless tokens that a React SPA can carry in headers
DatabaseSQLite (dev) β†’ PostgreSQL (later)SQLite needs zero setup for a weekend; Postgres for production
FrontendReact + ViteFast dev server; modern default (Create React App is deprecated)
HTTP clientaxiosInterceptors make attaching tokens and refreshing them clean
Routingreact-router-domStandard client-side routing with protected routes

πŸ’‘ Vite, not Create React App

Create React App is no longer maintained. For any new React project in 2026, reach for Vite (npm create vite@latest) or a framework like Next.js. This project uses Vite for a fast, modern dev experience.

System architecture

Two apps talk over HTTP/JSON. The React SPA never touches the database directly β€” it always goes through the API.

BookTracker system architecture A React single-page app in the browser sends JSON requests with a JWT to a Django REST API, which reads and writes a database and returns JSON responses. React SPA (the browser) Components + Router Auth Context axios client carries JWT in header Django REST API (the server) URLs / Routers ViewSets + Serializers JWT auth + permissions owner-scoped queries Database (storage) Users Books Sessions Β· Notes request JSON
Figure 1 β€” BookTracker is two cooperating apps. The React SPA carries a JWT on every request; Django validates it, scopes queries to the owner, and returns JSON.

The API contract

Agree the endpoints up front so backend and frontend can be built against the same "contract."

# Auth
POST   /api/auth/register/         Create an account
POST   /api/auth/token/            Log in β†’ { access, refresh }
POST   /api/auth/token/refresh/    Get a fresh access token

# Books (all owner-scoped, all require auth)
GET    /api/books/                 List my books  (?status=reading to filter)
POST   /api/books/                 Add a book
GET    /api/books/{id}/            One book
PUT    /api/books/{id}/            Update a book
DELETE /api/books/{id}/            Delete a book

# Nice-to-have
GET    /api/stats/reading/         Reading summary counts

The component tree

flowchart TD App --> Router Router --> Login Router --> Register Router --> Protected[Protected routes] Protected --> BookList Protected --> BookForm Protected --> BookDetail Protected --> Stats

πŸ’‘ Milestone 1 β€” Definition of Done

You have a data model, an endpoint list (the contract), and a component tree written down. You could hand these three artifacts to another developer and they'd know what to build.

Milestone 2 β€” Build the Backend

Build the API first and prove it works with curl before touching React. A trustworthy backend makes frontend work fast; a shaky one makes it miserable.

Step 1 β€” Project setup

python -m venv venv
source venv/bin/activate          # Windows: venv\Scripts\activate

pip install django djangorestframework djangorestframework-simplejwt django-cors-headers

django-admin startproject booktracker
cd booktracker
python manage.py startapp books
python manage.py startapp users

Step 2 β€” Configure settings

# booktracker/settings.py
from datetime import timedelta

INSTALLED_APPS = [
    # ...Django defaults...
    "rest_framework",
    "corsheaders",
    "books",
    "users",
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",   # must be high in the list
    # ...other middleware...
]

# Let the Vite dev server talk to us during development
CORS_ALLOWED_ORIGINS = ["http://localhost:5173"]

REST_FRAMEWORK = {
    "DEFAULT_AUTHENTICATION_CLASSES": [
        "rest_framework_simplejwt.authentication.JWTAuthentication",
    ],
    "DEFAULT_PERMISSION_CLASSES": [
        "rest_framework.permissions.IsAuthenticated",
    ],
}

SIMPLE_JWT = {
    "ACCESS_TOKEN_LIFETIME": timedelta(minutes=60),
    "REFRESH_TOKEN_LIFETIME": timedelta(days=14),
}

AUTH_USER_MODEL = "users.User"   # set this BEFORE the first migration

⚠️ Set the custom user model first

Swapping AUTH_USER_MODEL after you've run migrations is painful β€” Django bakes the user table in early. Decide on a custom user model now, before makemigrations, even if it starts almost empty.

Step 3 β€” Models

# users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models

class User(AbstractUser):
    bio = models.TextField(blank=True)

# books/models.py
from django.conf import settings
from django.core.validators import MinValueValidator, MaxValueValidator
from django.db import models

class Book(models.Model):
    class Status(models.TextChoices):
        TO_READ = "to_read", "To Read"
        READING = "reading", "Currently Reading"
        COMPLETED = "completed", "Completed"
        DNF = "dnf", "Did Not Finish"

    owner = models.ForeignKey(
        settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="books"
    )
    title = models.CharField(max_length=255)
    author = models.CharField(max_length=255)
    isbn = models.CharField(max_length=13, blank=True)
    pages = models.PositiveIntegerField(null=True, blank=True)
    status = models.CharField(
        max_length=20, choices=Status.choices, default=Status.TO_READ
    )
    date_added = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ["-date_added"]

    def __str__(self):
        return f"{self.title} by {self.author}"

class ReadingSession(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="sessions")
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    pages_read = models.PositiveIntegerField(default=0)

class Review(models.Model):
    book = models.OneToOneField(Book, on_delete=models.CASCADE, related_name="review")
    rating = models.PositiveSmallIntegerField(
        validators=[MinValueValidator(1), MaxValueValidator(5)]
    )
    content = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

πŸ“– Key terms

Serializer: DRF's translator between model instances and JSON. It also validates incoming data.

ViewSet: a class that bundles list/create/retrieve/update/delete for a resource, wired to a URL by a router.

Permission: a reusable rule (e.g. "must be the owner") that gates who may touch an object.

Step 4 β€” Serializers

# books/serializers.py
from rest_framework import serializers
from .models import Book

class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book
        fields = ["id", "title", "author", "isbn", "pages",
                  "status", "date_added"]
        read_only_fields = ["id", "date_added"]

# users/serializers.py
from rest_framework import serializers
from django.contrib.auth import get_user_model

User = get_user_model()

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True, min_length=8)

    class Meta:
        model = User
        fields = ["username", "email", "password"]

    def create(self, validated_data):
        # create_user hashes the password β€” never store it in plain text
        return User.objects.create_user(**validated_data)

Step 5 β€” Views & permissions

# books/views.py
from rest_framework import viewsets, permissions
from .models import Book
from .serializers import BookSerializer

class IsOwner(permissions.BasePermission):
    """Object-level: only the owner may touch their book."""
    def has_object_permission(self, request, view, obj):
        return obj.owner == request.user

class BookViewSet(viewsets.ModelViewSet):
    serializer_class = BookSerializer
    permission_classes = [permissions.IsAuthenticated, IsOwner]

    def get_queryset(self):
        # Never leak another user's books
        qs = Book.objects.filter(owner=self.request.user)
        status = self.request.query_params.get("status")
        return qs.filter(status=status) if status else qs

    def perform_create(self, serializer):
        serializer.save(owner=self.request.user)   # stamp the owner server-side

⚠️ Scope every query to the owner

The single most common security bug in apps like this is forgetting filter(owner=self.request.user), which lets any logged-in user read everyone's data. Set the owner on the server (never trust an owner field from the client) and always filter the queryset.

Step 6 β€” URLs

# users/urls.py
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from .views import RegisterView

urlpatterns = [
    path("register/", RegisterView.as_view(), name="register"),
    path("token/", TokenObtainPairView.as_view(), name="token"),
    path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]

# booktracker/urls.py
from django.contrib import admin
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from books.views import BookViewSet

router = DefaultRouter()
router.register(r"books", BookViewSet, basename="book")

urlpatterns = [
    path("admin/", admin.site.urls),
    path("api/auth/", include("users.urls")),
    path("api/", include(router.urls)),
]

Step 7 β€” Migrate & smoke-test

python manage.py makemigrations
python manage.py migrate
python manage.py runserver

# In another terminal β€” register, log in, then create a book:
curl -X POST http://localhost:8000/api/auth/register/ \
  -H "Content-Type: application/json" \
  -d '{"username":"ray","email":"ray@example.com","password":"supersecret"}'

curl -X POST http://localhost:8000/api/auth/token/ \
  -H "Content-Type: application/json" \
  -d '{"username":"ray","password":"supersecret"}'
# β†’ copy the "access" token from the response

curl http://localhost:8000/api/books/ \
  -H "Authorization: Bearer PASTE_ACCESS_TOKEN_HERE"

A successful empty shelf responds with:

[]

An empty array (not a 401) means auth works and the shelf is correctly owner-scoped. Now POST a book and confirm it comes back on the next GET.

πŸ’‘ Milestone 2 β€” Definition of Done

Register, token, and full book CRUD all work from curl or Postman. A request without a token is rejected with 401. Only your own books come back. Do not start React until this is true.

Milestone 3 β€” Build the Frontend

With a proven API, the React work is mostly wiring: hold the token, attach it to requests, and render the JSON.

Step 1 β€” Scaffold with Vite

npm create vite@latest booktracker-frontend -- --template react
cd booktracker-frontend
npm install
npm install axios react-router-dom
npm run dev            # serves at http://localhost:5173

Step 2 β€” An axios client that carries the token

One interceptor attaches the access token to every request; another transparently refreshes it on a 401 so users aren't kicked out mid-session.

// src/api/client.js
import axios from "axios";

const apiClient = axios.create({ baseURL: "http://localhost:8000/api" });

apiClient.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) config.headers.Authorization = `Bearer ${token}`;
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  async (error) => {
    const original = error.config;
    if (error.response?.status === 401 && !original._retry) {
      original._retry = true;
      const refresh = localStorage.getItem("refreshToken");
      if (!refresh) return Promise.reject(error);
      try {
        const { data } = await axios.post(
          "http://localhost:8000/api/auth/token/refresh/",
          { refresh }
        );
        localStorage.setItem("token", data.access);
        original.headers.Authorization = `Bearer ${data.access}`;
        return apiClient(original);        // replay the original request
      } catch (refreshError) {
        localStorage.removeItem("token");
        localStorage.removeItem("refreshToken");
        window.location.href = "/login";
        return Promise.reject(refreshError);
      }
    }
    return Promise.reject(error);
  }
);

export default apiClient;

Step 3 β€” An auth context

Context makes "who is logged in?" available anywhere without prop-drilling.

// src/context/AuthContext.jsx
import { createContext, useContext, useState } from "react";
import apiClient from "../api/client";

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
  const [user, setUser] = useState(() =>
    localStorage.getItem("token") ? { loggedIn: true } : null
  );

  async function login(username, password) {
    const { data } = await apiClient.post("/auth/token/", { username, password });
    localStorage.setItem("token", data.access);
    localStorage.setItem("refreshToken", data.refresh);
    setUser({ loggedIn: true });
  }

  function logout() {
    localStorage.removeItem("token");
    localStorage.removeItem("refreshToken");
    setUser(null);
  }

  return (
    
      {children}
    
  );
}

export const useAuth = () => useContext(AuthContext);

Step 4 β€” Protected routes

// src/routes/ProtectedRoute.jsx
import { Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";

export default function ProtectedRoute({ children }) {
  const { user } = useAuth();
  return user ? children : ;
}

// src/App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import ProtectedRoute from "./routes/ProtectedRoute";
import Login from "./pages/Login";
import BookList from "./pages/BookList";

export default function App() {
  return (
    
      
        
          } />
          
                
              
            }
          />
        
      
    
  );
}

Step 5 β€” The book list with a status filter

This one component demonstrates the whole loop: fetch on mount, re-fetch when the filter changes, and render the JSON.

// src/pages/BookList.jsx
import { useEffect, useState } from "react";
import apiClient from "../api/client";

const STATUSES = ["", "to_read", "reading", "completed"];

export default function BookList() {
  const [books, setBooks] = useState([]);
  const [filter, setFilter] = useState("");
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    apiClient
      .get("/books/", { params: filter ? { status: filter } : {} })
      .then((res) => setBooks(res.data))
      .catch((err) => console.error("Failed to load books", err))
      .finally(() => setLoading(false));
  }, [filter]);

  if (loading) return 

Loading your shelf…

; return (

My Shelf

{books.length === 0 ? (

No books yet β€” add your first one!

) : (
    {books.map((book) => (
  • {book.title} by {book.author} β€” {book.status}
  • ))}
)}
); }

βœ… Build in vertical slices

Get one feature working all the way through β€” add a book in the UI, watch it hit the API, see it saved, reload and confirm it persists β€” before building the next. A thin end-to-end slice teaches you more than three half-finished layers.

πŸ’‘ Milestone 3 β€” Definition of Done

In the browser you can log in, see your shelf, add a book, edit it, delete it, and filter by status β€” with loading and empty states handled. The must-have slice is complete.

Milestone 4 β€” Review & Extend

Polya's fourth step β€” "looking back" β€” is the one beginners skip and professionals treasure. Reviewing turns a working app into a good app and a finished weekend into durable learning.

Test the real journeys

Click through the app as a user, not a coder. Walk each user story end to end:

  • Register β†’ log in β†’ land on the shelf
  • Add a book β†’ it appears immediately and after a reload
  • Change a status β†’ the filter reflects it
  • Delete a book β†’ it's gone and stays gone
  • Log out β†’ protected routes bounce you to login
  • Wait for the access token to expire β†’ the refresh flow keeps you signed in
  • Shrink the window to phone width β†’ the layout still works

Reflect with Polya's four questions

StepAsk yourself
UnderstandDid I build what the user stories described β€” no more, no less?
PlanDid the schema and API contract hold up, or did I have to change them mid-build? Why?
ExecuteWhere did I deviate from the plan, and what did that teach me?
ReviewWhat one thing would I do differently on the next project?

Sensible extensions (pick one)

Only after the must-haves are solid. Each is a fresh mini-loop of understand β†’ plan β†’ execute β†’ review:

  • Reading sessions & notes β€” the nice-to-have models you already sketched.
  • Stats page β€” wire up /api/stats/reading/ and show counts by status.
  • ISBN lookup β€” autofill title/author from the Open Library API.
  • Pagination β€” turn on DRF pagination once your shelf grows.
  • Automated tests β€” a few DRF APITestCase tests for the book endpoints.
  • Deploy β€” Postgres + a host (Railway/Render for the API, Netlify/Vercel for the SPA).

Document it

A README.md is the difference between a project you can show and one you can't. Include: what it does, the stack, how to run backend and frontend locally, and a screenshot or two.

πŸ’‘ Milestone 4 β€” Definition of Done

Every user story passes a manual test, you've written a README, you've reflected on the four questions, and you've attempted (not necessarily finished) one extension.

Completion Checklist

Tick these off as you go. If you can check every must-have box, you've shipped a real full-stack app.

βœ… Must-have (the weekend goal)

  • Virtualenv created; Django + DRF + SimpleJWT + CORS installed
  • Custom user model set before the first migration
  • Registration, token, and refresh endpoints work
  • Book CRUD works and every query is owner-scoped
  • Requests without a valid token get 401
  • React app scaffolded with Vite; axios client attaches the token
  • Auth context + protected routes redirect logged-out users
  • Browser: log in, list, add, edit, delete, and filter books by status
  • Loading and empty states are handled (no blank screens)

πŸ’‘ Nice-to-have (bonus)

  • Reading sessions and notes per book
  • Review (1–5 rating) per book
  • Stats page with counts by status
  • Token refresh keeps you signed in after expiry
  • A few automated API tests
  • README with setup steps and screenshots
  • Deployed somewhere public

What Good Looks Like

"It runs" is the floor, not the bar. Here's how to tell a rushed weekend hack from a build you'd proudly link on your rΓ©sumΓ©.

DimensionJust okayWhat good looks like
SecurityEndpoints work when logged inEvery query owner-scoped; owner set server-side; no way to read others' data
Error handlingHappy path onlyFailed requests show a message; 401s trigger refresh or a clean redirect
UX statesBlank screen while loadingExplicit loading, empty, and error states everywhere data is fetched
Code shapeLogic copy-pasted per pageShared axios client & auth context; small, single-purpose components
Modern APIsDeprecated CRA, class componentsVite, function components + hooks, DRF viewsets/routers
DocumentationNo READMEREADME explains the app, the stack, and how to run both halves
πŸ’‘ The professional habit: a senior developer isn't someone who never has to review and fix their work β€” it's someone who always does. The "what good looks like" bar is just the review step made explicit.

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Polya's four steps β€” understand, plan, execute, review β€” turn a vague "build an app" into a controlled, repeatable process.
  • Milestones with a definition of done keep a multi-hour build from sprawling; reach a working state at each one.
  • Backend first, proven with curl β€” a trustworthy API makes the React work mostly wiring.
  • Owner-scope every query and set the owner server-side; it's the difference between private data and a leak.
  • Build vertical slices and handle loading/empty/error states β€” that's what separates "good" from "it runs."
  • Use modern tooling: Vite over the deprecated CRA, function components with hooks, DRF viewsets and routers.

🎯 Quick Quiz

Question 1: In Polya's method as applied here, which step is most responsible for preventing "solving the wrong problem"?

Question 2: Why must BookViewSet.get_queryset filter by owner=self.request.user?

Question 3: Why does this project scaffold the React app with Vite instead of Create React App?

πŸ“š Further Reading

πŸš€ What's Next?

You've built a full-stack app in Python and React and, more importantly, practiced a method you can reuse forever. Next we cross into the PHP world and unpack how the Laravel framework is architected β€” you'll see the same layers (routing, controllers, ORM, auth) wearing different clothes.

πŸŽ‰ You shipped it!

A working full-stack app and a repeatable process for the next one. That's a genuinely productive weekend.