Weekend Project: Building a Full-Stack Application with Python and React

Module 23: Web Frameworks II (Python)

Introduction to the Project

This weekend, you'll build a complete full-stack application that brings together everything we've learned about Python web frameworks and React. Rather than just following a step-by-step tutorial, we'll use George Polya's famous 4-step problem-solving approach to guide our development process. This methodology will not only help you build this project but will also equip you with a framework for tackling complex development challenges in your future career.

graph TD A[Problem Solving Process] --> B[1. Understand the Problem] A --> C[2. Devise a Plan] A --> D[3. Execute the Plan] A --> E[4. Review/Extend] B --> F[Full-Stack Application] C --> F D --> F E --> F F --> G[Python Backend] F --> H[React Frontend] style A fill:#f9f9f9,stroke:#333,stroke-width:2px style B fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style C fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style D fill:#fff3e0,stroke:#f57c00,stroke-width:2px style E fill:#fce4ec,stroke:#c2185b,stroke-width:2px style F fill:#ede7f6,stroke:#673ab7,stroke-width:2px style G fill:#e8eaf6,stroke:#3f51b5,stroke-width:2px style H fill:#e3f2fd,stroke:#2196f3,stroke-width:2px

Project Overview: BookTracker Application

You'll be building BookTracker, a personal library management system that allows users to:

George Polya's Problem Solving Approach

In his book "How to Solve It" (1945), mathematician George Polya outlined a four-step approach to problem solving that has become fundamental across disciplines. We'll apply this methodology to our software development process:

  1. Understand the Problem: Clarify requirements, identify constraints, and determine success criteria
  2. Devise a Plan: Break down the problem, choose technologies, design architecture and data models
  3. Execute the Plan: Implement solutions step by step, adapting as needed
  4. Review/Extend: Evaluate results, identify improvements, and extend functionality

This approach aligns perfectly with modern software development practices, encouraging deliberate planning before coding and reflective iteration after implementation.

Step 1: Understand the Problem

Before writing a single line of code, we need to thoroughly understand what we're trying to build. This phase is about questioning, clarifying, and defining our goals.

Key Questions to Ask

User Stories

Let's define some user stories to better understand specific needs:


1. As a user, I want to add books to my collection so I can track what I own.
2. As a user, I want to mark books as "to-read," "reading," or "completed" to track my progress.
3. As a user, I want to record the dates I started and finished books to maintain a reading history.
4. As a user, I want to add notes and reviews to books I've read to remember my thoughts.
5. As a user, I want to search and filter my collection to find specific books easily.
6. As a user, I want to view statistics about my reading habits to gain insights.
7. As a user, I want to access my library from different devices, so I need secure authentication.
            

Domain Understanding

For a book tracking application, we need to understand the domain:

classDiagram User "1" -- "many" Book : owns Book "1" -- "many" ReadingSession : has Book "1" -- "many" Note : has Book "1" -- "many" Review : has class User { +String username +String email +String password +Date joined } class Book { +String title +String author +String isbn +int pages +String status +Date dateAdded } class ReadingSession { +Date startDate +Date endDate +int pagesRead } class Note { +String content +Date created +int pageNumber } class Review { +int rating +String content +Date created }

Exercise: Refining Understanding

Take 15 minutes to further refine your understanding of the problem. Consider:

Write down your thoughts and bring them to our next discussion.

Step 2: Devise a Plan

With a clear understanding of our problem, we can now plan our approach. This involves technology selection, architecture design, and workflow planning.

Technology Stack

System Architecture

graph TD subgraph "Frontend (React)" A[Components] --> B[Context API] B --> C[Custom Hooks] C --> D[API Client] end subgraph "Backend (Django)" E[URLs/Routes] --> F[Views/ViewSets] F --> G[Serializers] G --> H[Models] H --> I[(Database)] J[Authentication] --> F end D <-->|HTTP/JSON| E style A fill:#61dafb,stroke:#333,stroke-width:2px style B fill:#61dafb,stroke:#333,stroke-width:2px style C fill:#61dafb,stroke:#333,stroke-width:2px style D fill:#61dafb,stroke:#333,stroke-width:2px style E fill:#44b78b,stroke:#333,stroke-width:2px style F fill:#44b78b,stroke:#333,stroke-width:2px style G fill:#44b78b,stroke:#333,stroke-width:2px style H fill:#44b78b,stroke:#333,stroke-width:2px style I fill:#336791,stroke:#333,stroke-width:2px style J fill:#44b78b,stroke:#333,stroke-width:2px

Data Models

Based on our domain understanding, we'll design the following models:


# Django Models
class User(AbstractUser):
    # Extended from Django's built-in User
    bio = models.TextField(blank=True)
    profile_image = models.ImageField(upload_to='profiles/', null=True, blank=True)

class Book(models.Model):
    STATUS_CHOICES = (
        ('to_read', 'To Read'),
        ('reading', 'Currently Reading'),
        ('completed', 'Completed'),
        ('dnf', 'Did Not Finish'),
    )
    
    owner = models.ForeignKey(User, 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)
    cover_image = models.URLField(blank=True)
    pages = models.PositiveIntegerField(null=True, blank=True)
    publication_year = models.PositiveSmallIntegerField(null=True, blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='to_read')
    date_added = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-date_added']

class ReadingSession(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reading_sessions')
    start_date = models.DateField()
    end_date = models.DateField(null=True, blank=True)
    pages_read = models.PositiveIntegerField(default=0)
    
    class Meta:
        ordering = ['-start_date']

class Note(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='notes')
    content = models.TextField()
    page_number = models.PositiveIntegerField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        ordering = ['-created_at']

class Review(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name='reviews')
    rating = models.PositiveSmallIntegerField(validators=[MinValueValidator(1), MaxValueValidator(5)])
    content = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        # One review per book
        unique_together = ['book']
        ordering = ['-created_at']
            

API Endpoints

Here's our planned REST API structure:


# Authentication
POST /api/auth/register/            # Create a new user account
POST /api/auth/token/               # Obtain JWT token
POST /api/auth/token/refresh/       # Refresh JWT token

# Users
GET /api/users/me/                  # Get current user profile
PUT /api/users/me/                  # Update current user profile

# Books
GET /api/books/                     # List all books (with filters)
POST /api/books/                    # Create a new book
GET /api/books/{id}/                # Retrieve a book
PUT /api/books/{id}/                # Update a book
DELETE /api/books/{id}/             # Delete a book

# Reading Sessions
GET /api/books/{id}/sessions/       # List reading sessions for a book
POST /api/books/{id}/sessions/      # Create reading session
PUT /api/sessions/{id}/             # Update reading session
DELETE /api/sessions/{id}/          # Delete reading session

# Notes
GET /api/books/{id}/notes/          # List notes for a book
POST /api/books/{id}/notes/         # Create note
PUT /api/notes/{id}/                # Update note
DELETE /api/notes/{id}/             # Delete note

# Reviews
GET /api/books/{id}/review/         # Get review for a book
POST /api/books/{id}/review/        # Create review
PUT /api/books/{id}/review/         # Update review
DELETE /api/books/{id}/review/      # Delete review

# Statistics
GET /api/stats/reading/             # Get reading statistics
            

Frontend Component Structure

Our React frontend will consist of these main components:

graph TD A[App] --> B[Routes] B --> C[AuthRoutes] B --> D[ProtectedRoutes] C --> E[Login] C --> F[Register] D --> G[Dashboard] D --> H[BookList] D --> I[BookDetail] D --> J[BookForm] D --> K[Profile] D --> L[Statistics] I --> M[NotesSection] I --> N[ReviewSection] I --> O[ReadingSessionSection] style A fill:#61dafb,stroke:#333,stroke-width:2px style B fill:#61dafb,stroke:#333,stroke-width:2px style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style D fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style E fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style F fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style G fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style H fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style I fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style J fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style K fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style L fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style M fill:#bbdefb,stroke:#1976d2,stroke-width:2px style N fill:#bbdefb,stroke:#1976d2,stroke-width:2px style O fill:#bbdefb,stroke:#1976d2,stroke-width:2px

Development Workflow

Let's break down our development plan into concrete steps:

  1. Set up project structures (Django project, React app)
  2. Implement data models and migrations
  3. Create serializers and API views
  4. Set up authentication
  5. Test backend API with Postman/curl
  6. Set up React project with routing
  7. Implement authentication UI and context
  8. Create book listing and detail views
  9. Implement book creation/editing
  10. Add reading sessions, notes, and reviews
  11. Create statistics views
  12. Finalize styling and UI polish
  13. Testing and bug fixes
  14. Documentation and review

We'll prioritize core functionality first (books and reading sessions) and add notes, reviews, and statistics if time allows.

Exercise: Plan Refinement

Take 20 minutes to review and refine this plan:

Sketch out a timeline for when you'll tackle each component of the plan.

Step 3: Execute the Plan

Now it's time to implement our solution following the plan we've devised. We'll break this into manageable steps, starting with the backend and then building the frontend.

Backend Development

Step 1: Project Setup


# Create virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install dependencies
pip install django djangorestframework djangorestframework-simplejwt django-cors-headers psycopg2-binary Pillow

# Create Django project
django-admin startproject booktracker
cd booktracker

# Create apps
python manage.py startapp books
python manage.py startapp users
            

Step 2: Configure Settings


# booktracker/settings.py
INSTALLED_APPS = [
    # ...
    'rest_framework',
    'corsheaders',
    'books',
    'users',
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    # ... other middleware
]

# Allow React dev server during development
CORS_ALLOWED_ORIGINS = [
    "http://localhost:3000",
]

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

# JWT Settings
from datetime import timedelta

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

# Custom user model
AUTH_USER_MODEL = 'users.User'
            

Step 3: Implement Models

Create the models as outlined in our plan:


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

class User(AbstractUser):
    bio = models.TextField(blank=True)
    profile_image = models.ImageField(upload_to='profiles/', null=True, blank=True)

# books/models.py
from django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator
from users.models import User

class Book(models.Model):
    STATUS_CHOICES = (
        ('to_read', 'To Read'),
        ('reading', 'Currently Reading'),
        ('completed', 'Completed'),
        ('dnf', 'Did Not Finish'),
    )
    
    owner = models.ForeignKey(User, 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)
    cover_image = models.URLField(blank=True)
    pages = models.PositiveIntegerField(null=True, blank=True)
    publication_year = models.PositiveSmallIntegerField(null=True, blank=True)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='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}"

# Implement ReadingSession, Note, and Review models as defined in our plan
            

Step 4: Create Serializers


# users/serializers.py
from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = ['id', 'username', 'email', 'bio', 'profile_image']
        read_only_fields = ['id', 'username', 'email']

class RegisterSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)
    
    class Meta:
        model = User
        fields = ['username', 'email', 'password']
    
    def create(self, validated_data):
        user = User.objects.create_user(
            username=validated_data['username'],
            email=validated_data['email'],
            password=validated_data['password']
        )
        return user

# books/serializers.py
from rest_framework import serializers
from .models import Book, ReadingSession, Note, Review

class ReadingSessionSerializer(serializers.ModelSerializer):
    class Meta:
        model = ReadingSession
        fields = ['id', 'book', 'start_date', 'end_date', 'pages_read']
        read_only_fields = ['id', 'book']

class NoteSerializer(serializers.ModelSerializer):
    class Meta:
        model = Note
        fields = ['id', 'book', 'content', 'page_number', 'created_at']
        read_only_fields = ['id', 'book', 'created_at']

class ReviewSerializer(serializers.ModelSerializer):
    class Meta:
        model = Review
        fields = ['id', 'book', 'rating', 'content', 'created_at']
        read_only_fields = ['id', 'book', 'created_at']

class BookSerializer(serializers.ModelSerializer):
    reading_sessions = ReadingSessionSerializer(many=True, read_only=True)
    notes = NoteSerializer(many=True, read_only=True)
    review = ReviewSerializer(read_only=True)
    
    class Meta:
        model = Book
        fields = [
            'id', 'title', 'author', 'isbn', 'cover_image', 'pages',
            'publication_year', 'status', 'date_added', 'reading_sessions',
            'notes', 'review'
        ]
        read_only_fields = ['id', 'date_added', 'owner']
    
    def create(self, validated_data):
        validated_data['owner'] = self.context['request'].user
        return super().create(validated_data)
            

Step 5: Implement Views and Endpoints


# users/views.py
from rest_framework import generics, permissions
from rest_framework.response import Response
from .models import User
from .serializers import UserSerializer, RegisterSerializer

class RegisterView(generics.CreateAPIView):
    queryset = User.objects.all()
    serializer_class = RegisterSerializer
    permission_classes = [permissions.AllowAny]

class UserProfileView(generics.RetrieveUpdateAPIView):
    serializer_class = UserSerializer
    permission_classes = [permissions.IsAuthenticated]
    
    def get_object(self):
        return self.request.user

# books/views.py
from rest_framework import viewsets, permissions, generics
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Book, ReadingSession, Note, Review
from .serializers import (
    BookSerializer, ReadingSessionSerializer, 
    NoteSerializer, ReviewSerializer
)

class IsOwner(permissions.BasePermission):
    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):
        queryset = Book.objects.filter(owner=self.request.user)
        status = self.request.query_params.get('status')
        if status:
            queryset = queryset.filter(status=status)
        return queryset
    
    @action(detail=True, methods=['get'])
    def reading_sessions(self, request, pk=None):
        book = self.get_object()
        sessions = book.reading_sessions.all()
        serializer = ReadingSessionSerializer(sessions, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get'])
    def notes(self, request, pk=None):
        book = self.get_object()
        notes = book.notes.all()
        serializer = NoteSerializer(notes, many=True)
        return Response(serializer.data)
    
    @action(detail=True, methods=['get', 'post', 'put', 'delete'])
    def review(self, request, pk=None):
        book = self.get_object()
        
        if request.method == 'GET':
            try:
                review = book.reviews.get()
                serializer = ReviewSerializer(review)
                return Response(serializer.data)
            except Review.DoesNotExist:
                return Response({'detail': 'Review not found'}, status=404)
        
        elif request.method == 'POST':
            serializer = ReviewSerializer(data=request.data)
            if serializer.is_valid():
                serializer.save(book=book)
                return Response(serializer.data, status=201)
            return Response(serializer.errors, status=400)
        
        elif request.method == 'PUT':
            try:
                review = book.reviews.get()
                serializer = ReviewSerializer(review, data=request.data)
                if serializer.is_valid():
                    serializer.save()
                    return Response(serializer.data)
                return Response(serializer.errors, status=400)
            except Review.DoesNotExist:
                return Response({'detail': 'Review not found'}, status=404)
        
        elif request.method == 'DELETE':
            try:
                review = book.reviews.get()
                review.delete()
                return Response(status=204)
            except Review.DoesNotExist:
                return Response({'detail': 'Review not found'}, status=404)

# Implement similar ViewSets for ReadingSession and Note

# Statistics view
@action(detail=False, methods=['get'])
def statistics(self, request):
    books = Book.objects.filter(owner=request.user)
    total_books = books.count()
    completed_books = books.filter(status='completed').count()
    reading_books = books.filter(status='reading').count()
    to_read_books = books.filter(status='to_read').count()
    
    # Calculate pages read
    total_pages_read = 0
    for book in books.filter(status='completed'):
        if book.pages:
            total_pages_read += book.pages
    
    # Get reading sessions
    reading_sessions = ReadingSession.objects.filter(book__owner=request.user)
    
    # Calculate reading streak
    # This would be more complex in a real application
    
    return Response({
        'total_books': total_books,
        'completed_books': completed_books,
        'reading_books': reading_books,
        'to_read_books': to_read_books,
        'total_pages_read': total_pages_read,
    })
            

Step 6: Configure URLs


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

urlpatterns = [
    path('register/', RegisterView.as_view(), name='register'),
    path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
    path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
    path('me/', UserProfileView.as_view(), name='user-profile'),
]

# books/urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import BookViewSet, ReadingSessionViewSet, NoteViewSet

router = DefaultRouter()
router.register(r'books', BookViewSet, basename='book')
router.register(r'sessions', ReadingSessionViewSet, basename='session')
router.register(r'notes', NoteViewSet, basename='note')

urlpatterns = [
    path('', include(router.urls)),
    path('stats/reading/', statistics, name='reading-stats'),
]

# booktracker/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static

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

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
            

Step 7: Create Migrations and Test API


# Create migrations
python manage.py makemigrations
python manage.py migrate

# Create a superuser
python manage.py createsuperuser

# Run the server
python manage.py runserver
            

Test the API endpoints using tools like Postman or curl before proceeding to frontend development.

Frontend Development

Step 1: Create React App


# Create React project
npx create-react-app booktracker-frontend
cd booktracker-frontend

# Install dependencies
npm install react-router-dom axios formik yup react-icons date-fns tailwindcss
            

Step 2: Configure Tailwind CSS


# Initialize Tailwind
npx tailwindcss init

# In tailwind.config.js
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}",
  ],
  theme: {
    extend: {
      colors: {
        primary: "#4a5568",
        secondary: "#718096",
      },
    },
  },
  plugins: [],
}

# In src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
            

Step 3: Set Up Context API for Authentication


// src/context/AuthContext.js
import React, { createContext, useState, useContext, useEffect } from 'react';
import axios from 'axios';

const AuthContext = createContext();

export const AuthProvider = ({ children }) => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  
  useEffect(() => {
    // Check if token exists
    const token = localStorage.getItem('token');
    if (token) {
      fetchUserProfile();
    } else {
      setLoading(false);
    }
  }, []);
  
  const fetchUserProfile = async () => {
    try {
      const response = await axios.get('http://localhost:8000/api/auth/me/', {
        headers: {
          'Authorization': `Bearer ${localStorage.getItem('token')}`
        }
      });
      setUser(response.data);
    } catch (error) {
      console.error("Error fetching user profile:", error);
      logout();
    } finally {
      setLoading(false);
    }
  };
  
  const login = async (username, password) => {
    try {
      setError(null);
      const response = await axios.post('http://localhost:8000/api/auth/token/', {
        username,
        password
      });
      
      localStorage.setItem('token', response.data.access);
      localStorage.setItem('refreshToken', response.data.refresh);
      
      await fetchUserProfile();
      return true;
    } catch (error) {
      console.error("Login error:", error);
      setError(error.response?.data?.detail || 'Login failed');
      return false;
    }
  };
  
  const register = async (username, email, password) => {
    try {
      setError(null);
      await axios.post('http://localhost:8000/api/auth/register/', {
        username,
        email,
        password
      });
      
      // Login after successful registration
      return await login(username, password);
    } catch (error) {
      console.error("Registration error:", error);
      setError(error.response?.data?.detail || 'Registration failed');
      return false;
    }
  };
  
  const logout = () => {
    localStorage.removeItem('token');
    localStorage.removeItem('refreshToken');
    setUser(null);
  };
  
  const isAuthenticated = () => !!user;
  
  return (
    
      {children}
    
  );
};

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

Step 4: Set Up API Client


// 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;
  },
  error => {
    return Promise.reject(error);
  }
);

apiClient.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // If error is 401 and not a retry
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      
      try {
        // Try to refresh token
        const refreshToken = localStorage.getItem('refreshToken');
        if (!refreshToken) {
          throw new Error('No refresh token');
        }
        
        const response = await axios.post('http://localhost:8000/api/auth/token/refresh/', {
          refresh: refreshToken
        });
        
        const newToken = response.data.access;
        localStorage.setItem('token', newToken);
        
        // Retry original request with new token
        originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
        return axios(originalRequest);
      } catch (refreshError) {
        // If refresh fails, logout user
        localStorage.removeItem('token');
        localStorage.removeItem('refreshToken');
        window.location = '/login';
        return Promise.reject(refreshError);
      }
    }
    
    return Promise.reject(error);
  }
);

export default apiClient;
            

Step 5: Create Book API Service


// src/api/bookService.js
import apiClient from './client';

export const getBooks = async (filters = {}) => {
  try {
    const queryParams = new URLSearchParams();
    Object.entries(filters).forEach(([key, value]) => {
      if (value) queryParams.append(key, value);
    });
    
    const response = await apiClient.get(`/books/?${queryParams}`);
    return response.data;
  } catch (error) {
    console.error('Error fetching books:', error);
    throw error;
  }
};

export const getBook = async (id) => {
  try {
    const response = await apiClient.get(`/books/${id}/`);
    return response.data;
  } catch (error) {
    console.error(`Error fetching book ${id}:`, error);
    throw error;
  }
};

export const createBook = async (bookData) => {
  try {
    const response = await apiClient.post('/books/', bookData);
    return response.data;
  } catch (error) {
    console.error('Error creating book:', error);
    throw error;
  }
};

export const updateBook = async (id, bookData) => {
  try {
    const response = await apiClient.put(`/books/${id}/`, bookData);
    return response.data;
  } catch (error) {
    console.error(`Error updating book ${id}:`, error);
    throw error;
  }
};

export const deleteBook = async (id) => {
  try {
    await apiClient.delete(`/books/${id}/`);
    return id;
  } catch (error) {
    console.error(`Error deleting book ${id}:`, error);
    throw error;
  }
};

// Add more functions for reading sessions, notes, reviews, and statistics
            

Step 6: Set Up Routes


// src/App.js
import React from 'react';
import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-dom';
import { AuthProvider, useAuth } from './context/AuthContext';

// Import pages
import Login from './pages/Login';
import Register from './pages/Register';
import Dashboard from './pages/Dashboard';
import BookList from './pages/BookList';
import BookDetail from './pages/BookDetail';
import BookForm from './pages/BookForm';
import Profile from './pages/Profile';
import Statistics from './pages/Statistics';
import NotFound from './pages/NotFound';

// Protected route component
const ProtectedRoute = ({ children }) => {
  const { isAuthenticated, loading } = useAuth();
  
  if (loading) {
    return 
Loading...
; } if (!isAuthenticated()) { return ; } return children; }; function App() { return (
} /> } /> } /> } /> } /> } /> } /> } /> } /> } />
); } export default App;

Step 7: Implement Authentication Pages


// src/pages/Login.js
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import * as Yup from 'yup';

const LoginSchema = Yup.object().shape({
  username: Yup.string().required('Username is required'),
  password: Yup.string().required('Password is required'),
});

const Login = () => {
  const { login, error, isAuthenticated } = useAuth();
  const navigate = useNavigate();
  
  // Redirect if already authenticated
  if (isAuthenticated()) {
    navigate('/');
    return null;
  }
  
  const handleSubmit = async (values, { setSubmitting }) => {
    const success = await login(values.username, values.password);
    if (success) {
      navigate('/');
    }
    setSubmitting(false);
  };
  
  return (
    

Sign in to BookTracker

Or{' '} create a new account

{error && (
{error}
)} {({ isSubmitting }) => (
)}
); }; export default Login;

Follow similar patterns to implement the remaining components. Due to space constraints, we can't include all component implementations here, but the core patterns remain consistent.

Key Components to Implement:

Exercise: Implementation Focus

For the next 2-3 hours, focus on implementing one core part of the application:

  1. Complete the backend setup following the code examples
  2. Implement the authentication flow and book listing features in the frontend
  3. Make sure to test as you go to catch issues early

Remember to take breaks and review your progress regularly.

Step 4: Review and Extend

After implementing the core functionality, it's time to review our work, identify improvements, and consider extensions.

Code Review and Testing

Follow this checklist to ensure the quality of your implementation:


Backend Review:
✅ API endpoints working as expected
✅ Authentication flow secure and functional
✅ Data models and relationships properly set up
✅ Permissions enforcing proper access control
✅ Error handling providing useful messages
✅ Performance considerations (indexes, query optimization)

Frontend Review:
✅ Authentication flow working smoothly
✅ API integration properly handling success/error states
✅ Components properly structured and reusable
✅ State management clean and predictable
✅ UI responsive and accessible
✅ Error states handled gracefully
✅ Loading states implemented
            

Manual testing scenarios:

Possible Improvements

Consider these improvements if time allows:

Backend Improvements

Frontend Improvements

Extensions

If you've completed the core functionality and improvements, consider these extensions:

Feature Extensions

Reflection Using Polya's Method

Now that we've completed the implementation, let's reflect on our problem-solving journey:

1. Understanding the Problem

2. Devising a Plan

3. Executing the Plan

4. Looking Back

Exercise: Final Review and Documentation

As a final exercise, create documentation for your project:

  1. Create a README.md file explaining project setup and features
  2. Document API endpoints for future reference
  3. Create a reflection document about your learning experience
  4. Take screenshots of key features for your portfolio

Applying Polya's Method to Software Development

Through this weekend project, we've seen how George Polya's problem-solving approach can be adapted to software development. Let's examine how each step maps to development practices:

graph LR A[Polya's Method] --- B[Software Development] C[1. Understand] --- D[Requirements Analysis] E[2. Plan] --- F[Architecture & Design] G[3. Execute] --- H[Implementation] I[4. Review] --- J[Testing & Refinement] style A fill:#f9f9f9,stroke:#333,stroke-width:2px style B fill:#f9f9f9,stroke:#333,stroke-width:2px style C fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style D fill:#e3f2fd,stroke:#1976d2,stroke-width:2px style E fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style F fill:#e8f5e9,stroke:#388e3c,stroke-width:2px style G fill:#fff3e0,stroke:#f57c00,stroke-width:2px style H fill:#fff3e0,stroke:#f57c00,stroke-width:2px style I fill:#fce4ec,stroke:#c2185b,stroke-width:2px style J fill:#fce4ec,stroke:#c2185b,stroke-width:2px

Understanding Phase Benefits

Planning Phase Benefits

Execution Phase Benefits

Review Phase Benefits

Applying to Other Projects

This problem-solving framework can be applied to projects of any size:

Project Size Understand Plan Execute Review
Small (1 day) 15-30 minutes 30-60 minutes 4-6 hours 30 minutes
Medium (2 weeks) 1-2 days 2-3 days 1-1.5 weeks 1-2 days
Large (3+ months) 1-2 weeks 2-3 weeks 2-3 months 2-3 weeks

Remember that these phases often overlap and may be revisited as development progresses. The key is to maintain the deliberate, thoughtful approach that Polya advocated.

Practice Activities

Basic Exercise: Weather Dashboard

Apply Polya's method to build a simple weather dashboard that integrates with a weather API. Focus on following all four steps in the problem-solving process.

Intermediate Exercise: Task Management System

Create a task management system with features like categories, priorities, due dates, and status tracking. Use Polya's method to guide your approach.

Advanced Exercise: E-Commerce Platform

Build a small e-commerce platform with product listings, shopping cart, checkout process, and order management. Document how you apply each step of Polya's method.

Challenge: Extend BookTracker

Take the BookTracker application and implement one of the extension ideas (book import, reading goals, etc.). Follow Polya's approach for this new feature development.

Conclusion

In this weekend project, we've built a full-stack BookTracker application while following George Polya's problem-solving methodology. By breaking down the process into understanding, planning, executing, and reviewing, we've created a more thoughtful and structured approach to development.

The skills you've practiced here—deliberate problem definition, careful planning, systematic implementation, and reflective review—will serve you well in all future development projects, regardless of scale or technology stack.

Remember that becoming a skilled developer isn't just about knowing languages and frameworks; it's about developing a systematic approach to solving problems. Polya's method provides exactly that framework, helping you build not just functioning code, but elegant solutions to real-world problems.

Further Resources