π οΈ 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.
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.
| Milestone | Focus | Done when⦠| Rough time |
|---|---|---|---|
| M0 β Understand | Requirements & user stories | You can state the app in one sentence and list its user stories | 30 min |
| M1 β Plan | Stack, data model, API, components | You have a schema, an endpoint list, and a component tree on paper | 1 hr |
| M2 β Backend | Django REST API + auth | Every endpoint returns correct JSON, tested with curl/Postman | 3β4 hrs |
| M3 β Frontend | React UI + API integration | You can register, log in, and do full CRUD on books in the browser | 3β4 hrs |
| M4 β Review | Test, polish, extend, document | Checklist passes; README written; one extension attempted | 1β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.
π‘ 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
| Layer | Choice | Why |
|---|---|---|
| Backend framework | Django + Django REST Framework | Batteries-included; serializers & viewsets remove boilerplate |
| Auth | JWT (SimpleJWT) | Stateless tokens that a React SPA can carry in headers |
| Database | SQLite (dev) β PostgreSQL (later) | SQLite needs zero setup for a weekend; Postgres for production |
| Frontend | React + Vite | Fast dev server; modern default (Create React App is deprecated) |
| HTTP client | axios | Interceptors make attaching tokens and refreshing them clean |
| Routing | react-router-dom | Standard 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.
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
π‘ 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
| Step | Ask yourself |
|---|---|
| Understand | Did I build what the user stories described β no more, no less? |
| Plan | Did the schema and API contract hold up, or did I have to change them mid-build? Why? |
| Execute | Where did I deviate from the plan, and what did that teach me? |
| Review | What 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
APITestCasetests 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Γ©.
| Dimension | Just okay | What good looks like |
|---|---|---|
| Security | Endpoints work when logged in | Every query owner-scoped; owner set server-side; no way to read others' data |
| Error handling | Happy path only | Failed requests show a message; 401s trigger refresh or a clean redirect |
| UX states | Blank screen while loading | Explicit loading, empty, and error states everywhere data is fetched |
| Code shape | Logic copy-pasted per page | Shared axios client & auth context; small, single-purpose components |
| Modern APIs | Deprecated CRA, class components | Vite, function components + hooks, DRF viewsets/routers |
| Documentation | No README | README 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
- George Polya β "How to Solve It"
- Django REST Framework documentation
- SimpleJWT β JWT auth for DRF
- React β official learn guide
- Vite β getting started
π 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.