# ThrillWiki Frontend Connection Guide A comprehensive guide for connecting any frontend framework to the ThrillWiki Django REST API backend. ## Table of Contents 1. [Overview](#overview) 2. [API Architecture](#api-architecture) 3. [Authentication System](#authentication-system) 4. [Core API Endpoints](#core-api-endpoints) 5. [Frontend Framework Examples](#frontend-framework-examples) 6. [Dark/Light Mode Requirements](#darklight-mode-requirements) 7. [Error Handling](#error-handling) 8. [Best Practices](#best-practices) 9. [Troubleshooting](#troubleshooting) ## Overview ThrillWiki uses a modern Django REST Framework backend with a centralized API architecture. All API endpoints are organized under `/api/v1/` with domain-specific modules for parks, rides, authentication, and more. ### Key Features - **Centralized API Structure** - All endpoints under `/api/v1/` - **Token-based Authentication** - Using Django REST Framework tokens - **Comprehensive Serialization** - Well-structured JSON responses - **CORS Support** - Configured for frontend integration - **OpenAPI Documentation** - Auto-generated API docs at `/api/docs/` ### Base Configuration ```javascript const API_BASE_URL = 'http://localhost:8000/api/v1' ``` ## API Architecture ### Centralized Structure All API endpoints follow this pattern: ``` /api/v1/{domain}/{endpoint}/ ``` ### Domain Organization - **Authentication**: `/api/v1/auth/` - **Parks**: `/api/v1/parks/` - **Rides**: `/api/v1/rides/` - **Accounts**: `/api/v1/accounts/` - **Health**: `/api/v1/health/` ### URL Routing The main API router is located at `backend/apps/api/v1/urls.py`: ```python # Main API v1 URLs urlpatterns = [ path("auth/", include("apps.api.v1.auth.urls")), path("health/", include("apps.api.v1.health.urls")), path("trending/", include("apps.api.v1.trending.urls")), path("parks/", include("apps.api.v1.parks.urls")), path("rides/", include("apps.api.v1.rides.urls")), path("accounts/", include("apps.api.v1.accounts.urls")), ] ``` ## Authentication System ### Token-Based Authentication ThrillWiki uses Django REST Framework's token authentication system. #### Authentication Flow 1. **Login/Signup** - Obtain authentication token 2. **Store Token** - Save token securely (localStorage, sessionStorage, or cookies) 3. **Include Token** - Add token to all authenticated requests 4. **Handle Expiration** - Refresh or re-authenticate when token expires #### Authentication Endpoints | Endpoint | Method | Purpose | Authentication Required | |----------|--------|---------|------------------------| | `/api/v1/auth/login/` | POST | User login | No | | `/api/v1/auth/signup/` | POST | User registration | No | | `/api/v1/auth/logout/` | POST | User logout | Yes | | `/api/v1/auth/current-user/` | GET | Get current user info | Yes | | `/api/v1/auth/password-reset/` | POST | Request password reset | No | | `/api/v1/auth/password-change/` | POST | Change password | Yes | | `/api/v1/auth/status/` | POST | Check auth status | No | | `/api/v1/auth/social-providers/` | GET | Get social auth providers | No | #### Request/Response Examples **Login Request:** ```json { "username": "thrillseeker", "password": "securepassword123" } ``` **Login Response:** ```json { "token": "abc123def456ghi789", "user": { "id": 1, "username": "thrillseeker", "email": "user@example.com", "first_name": "John", "last_name": "Doe", "is_active": true, "date_joined": "2024-01-01T00:00:00Z" }, "message": "Login successful" } ``` **Signup Request:** ```json { "username": "newuser", "email": "newuser@example.com", "password": "securepassword123", "password_confirm": "securepassword123", "first_name": "Jane", "last_name": "Smith" } ``` ## Core API Endpoints ### Parks API (`/api/v1/parks/`) | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/v1/parks/` | GET | List all parks | | `/api/v1/parks/` | POST | Create new park | | `/api/v1/parks/{id}/` | GET | Get park details | | `/api/v1/parks/{id}/` | PUT/PATCH | Update park | | `/api/v1/parks/{id}/` | DELETE | Delete park | | `/api/v1/parks/search/` | GET | Search parks | | `/api/v1/parks/filter/` | GET | Filter parks | | `/api/v1/parks/{id}/photos/` | GET/POST | Park photos | ### Rides API (`/api/v1/rides/`) | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/v1/rides/` | GET | List all rides | | `/api/v1/rides/` | POST | Create new ride | | `/api/v1/rides/{id}/` | GET | Get ride details | | `/api/v1/rides/{id}/` | PUT/PATCH | Update ride | | `/api/v1/rides/{id}/` | DELETE | Delete ride | | `/api/v1/rides/search/` | GET | Search rides | | `/api/v1/rides/filter/` | GET | Filter rides | | `/api/v1/rides/{id}/photos/` | GET/POST | Ride photos | ### Health Check (`/api/v1/health/`) | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/v1/health/` | GET | Basic health check | | `/api/v1/health/detailed/` | GET | Detailed system status | ## Frontend Framework Examples ### Vanilla JavaScript ```javascript class ThrillWikiAPI { constructor(baseURL = 'http://localhost:8000/api/v1') { this.baseURL = baseURL; this.token = localStorage.getItem('thrillwiki_token'); } // Set authentication token setToken(token) { this.token = token; localStorage.setItem('thrillwiki_token', token); } // Remove authentication token clearToken() { this.token = null; localStorage.removeItem('thrillwiki_token'); } // Make authenticated request async request(endpoint, options = {}) { const url = `${this.baseURL}${endpoint}`; const headers = { 'Content-Type': 'application/json', ...options.headers }; if (this.token) { headers['Authorization'] = `Token ${this.token}`; } const response = await fetch(url, { ...options, headers }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); } // Authentication methods async login(username, password) { const response = await this.request('/auth/login/', { method: 'POST', body: JSON.stringify({ username, password }) }); if (response.token) { this.setToken(response.token); } return response; } async signup(userData) { const response = await this.request('/auth/signup/', { method: 'POST', body: JSON.stringify(userData) }); if (response.token) { this.setToken(response.token); } return response; } async logout() { try { await this.request('/auth/logout/', { method: 'POST' }); } finally { this.clearToken(); } } async getCurrentUser() { return this.request('/auth/current-user/'); } // Parks methods async getParks(params = {}) { const queryString = new URLSearchParams(params).toString(); return this.request(`/parks/${queryString ? '?' + queryString : ''}`); } async getPark(id) { return this.request(`/parks/${id}/`); } async createPark(parkData) { return this.request('/parks/', { method: 'POST', body: JSON.stringify(parkData) }); } // Rides methods async getRides(params = {}) { const queryString = new URLSearchParams(params).toString(); return this.request(`/rides/${queryString ? '?' + queryString : ''}`); } async getRide(id) { return this.request(`/rides/${id}/`); } async createRide(rideData) { return this.request('/rides/', { method: 'POST', body: JSON.stringify(rideData) }); } } // Usage example const api = new ThrillWikiAPI(); // Login api.login('username', 'password') .then(response => { console.log('Logged in:', response.user); return api.getParks(); }) .then(parks => { console.log('Parks:', parks); }) .catch(error => { console.error('Error:', error); }); ``` ### React with Axios ```jsx import axios from 'axios'; import { createContext, useContext, useState, useEffect } from 'react'; // API Configuration const API_BASE_URL = 'http://localhost:8000/api/v1'; const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }); // Request interceptor to add auth token api.interceptors.request.use((config) => { const token = localStorage.getItem('thrillwiki_token'); if (token) { config.headers.Authorization = `Token ${token}`; } return config; }); // Response interceptor for error handling api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { localStorage.removeItem('thrillwiki_token'); window.location.href = '/login'; } return Promise.reject(error); } ); // Auth Context const AuthContext = createContext(); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const token = localStorage.getItem('thrillwiki_token'); if (token) { getCurrentUser(); } else { setLoading(false); } }, []); const getCurrentUser = async () => { try { const response = await api.get('/auth/current-user/'); setUser(response.data); } catch (error) { localStorage.removeItem('thrillwiki_token'); } finally { setLoading(false); } }; const login = async (username, password) => { const response = await api.post('/auth/login/', { username, password }); localStorage.setItem('thrillwiki_token', response.data.token); setUser(response.data.user); return response.data; }; const signup = async (userData) => { const response = await api.post('/auth/signup/', userData); localStorage.setItem('thrillwiki_token', response.data.token); setUser(response.data.user); return response.data; }; const logout = async () => { try { await api.post('/auth/logout/'); } finally { localStorage.removeItem('thrillwiki_token'); setUser(null); } }; return ( {children} ); }; export const useAuth = () => useContext(AuthContext); // API Service export const apiService = { // Parks getParks: (params) => api.get('/parks/', { params }), getPark: (id) => api.get(`/parks/${id}/`), createPark: (data) => api.post('/parks/', data), updatePark: (id, data) => api.patch(`/parks/${id}/`, data), deletePark: (id) => api.delete(`/parks/${id}/`), // Rides getRides: (params) => api.get('/rides/', { params }), getRide: (id) => api.get(`/rides/${id}/`), createRide: (data) => api.post('/rides/', data), updateRide: (id, data) => api.patch(`/rides/${id}/`, data), deleteRide: (id) => api.delete(`/rides/${id}/`), }; // Component Example const ParksList = () => { const [parks, setParks] = useState([]); const [loading, setLoading] = useState(true); const { user } = useAuth(); useEffect(() => { const fetchParks = async () => { try { const response = await apiService.getParks(); setParks(response.data.results || response.data); } catch (error) { console.error('Error fetching parks:', error); } finally { setLoading(false); } }; fetchParks(); }, []); if (loading) return
Loading parks...
; return (

Theme Parks

{parks.map(park => (

{park.name}

{park.description}

Location: {park.location}

))}
); }; ``` ### Vue.js 3 with Composition API ```vue ``` **Vue Composable (`composables/useApi.js`):** ```javascript import axios from 'axios' import { ref, computed } from 'vue' const API_BASE_URL = 'http://localhost:8000/api/v1' const api = axios.create({ baseURL: API_BASE_URL, headers: { 'Content-Type': 'application/json', }, }) // Global state const user = ref(null) const token = ref(localStorage.getItem('thrillwiki_token')) // Request interceptor api.interceptors.request.use((config) => { if (token.value) { config.headers.Authorization = `Token ${token.value}` } return config }) // Response interceptor api.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { token.value = null user.value = null localStorage.removeItem('thrillwiki_token') } return Promise.reject(error) } ) export function useApi() { const isAuthenticated = computed(() => !!token.value) const setToken = (newToken) => { token.value = newToken localStorage.setItem('thrillwiki_token', newToken) } const clearToken = () => { token.value = null user.value = null localStorage.removeItem('thrillwiki_token') } const login = async (username, password) => { const response = await api.post('/auth/login/', { username, password }) setToken(response.data.token) user.value = response.data.user return response.data } const signup = async (userData) => { const response = await api.post('/auth/signup/', userData) setToken(response.data.token) user.value = response.data.user return response.data } const logout = async () => { try { await api.post('/auth/logout/') } finally { clearToken() } } const getCurrentUser = async () => { const response = await api.get('/auth/current-user/') user.value = response.data return response.data } const apiService = { // Parks getParks: (params) => api.get('/parks/', { params }), getPark: (id) => api.get(`/parks/${id}/`), createPark: (data) => api.post('/parks/', data), updatePark: (id, data) => api.patch(`/parks/${id}/`, data), deletePark: (id) => api.delete(`/parks/${id}/`), // Rides getRides: (params) => api.get('/rides/', { params }), getRide: (id) => api.get(`/rides/${id}/`), createRide: (data) => api.post('/rides/', data), updateRide: (id, data) => api.patch(`/rides/${id}/`, data), deleteRide: (id) => api.delete(`/rides/${id}/`), } return { user: readonly(user), isAuthenticated, login, signup, logout, getCurrentUser, apiService } } ``` ### Angular with HttpClient ```typescript // api.service.ts import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, BehaviorSubject } from 'rxjs'; import { tap } from 'rxjs/operators'; export interface User { id: number; username: string; email: string; first_name: string; last_name: string; is_active: boolean; date_joined: string; } export interface LoginResponse { token: string; user: User; message: string; } @Injectable({ providedIn: 'root' }) export class ApiService { private baseURL = 'http://localhost:8000/api/v1'; private tokenSubject = new BehaviorSubject( localStorage.getItem('thrillwiki_token') ); private userSubject = new BehaviorSubject(null); public token$ = this.tokenSubject.asObservable(); public user$ = this.userSubject.asObservable(); constructor(private http: HttpClient) { // Initialize user if token exists if (this.tokenSubject.value) { this.getCurrentUser().subscribe(); } } private getHeaders(): HttpHeaders { const token = this.tokenSubject.value; let headers = new HttpHeaders({ 'Content-Type': 'application/json' }); if (token) { headers = headers.set('Authorization', `Token ${token}`); } return headers; } // Authentication methods login(username: string, password: string): Observable { return this.http.post(`${this.baseURL}/auth/login/`, { username, password }).pipe( tap(response => { localStorage.setItem('thrillwiki_token', response.token); this.tokenSubject.next(response.token); this.userSubject.next(response.user); }) ); } signup(userData: any): Observable { return this.http.post(`${this.baseURL}/auth/signup/`, userData) .pipe( tap(response => { localStorage.setItem('thrillwiki_token', response.token); this.tokenSubject.next(response.token); this.userSubject.next(response.user); }) ); } logout(): Observable { return this.http.post(`${this.baseURL}/auth/logout/`, {}, { headers: this.getHeaders() }).pipe( tap(() => { localStorage.removeItem('thrillwiki_token'); this.tokenSubject.next(null); this.userSubject.next(null); }) ); } getCurrentUser(): Observable { return this.http.get(`${this.baseURL}/auth/current-user/`, { headers: this.getHeaders() }).pipe( tap(user => this.userSubject.next(user)) ); } // Parks methods getParks(params?: any): Observable { let httpParams = new HttpParams(); if (params) { Object.keys(params).forEach(key => { httpParams = httpParams.set(key, params[key]); }); } return this.http.get(`${this.baseURL}/parks/`, { headers: this.getHeaders(), params: httpParams }); } getPark(id: number): Observable { return this.http.get(`${this.baseURL}/parks/${id}/`, { headers: this.getHeaders() }); } createPark(parkData: any): Observable { return this.http.post(`${this.baseURL}/parks/`, parkData, { headers: this.getHeaders() }); } // Rides methods getRides(params?: any): Observable { let httpParams = new HttpParams(); if (params) { Object.keys(params).forEach(key => { httpParams = httpParams.set(key, params[key]); }); } return this.http.get(`${this.baseURL}/rides/`, { headers: this.getHeaders(), params: httpParams }); } getRide(id: number): Observable { return this.http.get(`${this.baseURL}/rides/${id}/`, { headers: this.getHeaders() }); } createRide(rideData: any): Observable { return this.http.post(`${this.baseURL}/rides/`, rideData, { headers: this.getHeaders() }); } } ``` **Component Example:** ```typescript // parks-list.component.ts import { Component, OnInit } from '@angular/core'; import { ApiService } from '../services/api.service'; @Component({ selector: 'app-parks-list', template: `

Theme Parks

Loading parks...

{{ park.name }}

{{ park.description }}

Location: {{ park.location }}

` }) export class ParksListComponent implements OnInit { parks: any[] = []; loading = true; constructor(private apiService: ApiService) {} ngOnInit() { this.apiService.getParks().subscribe({ next: (response) => { this.parks = response.results || response; this.loading = false; }, error: (error) => { console.error('Error fetching parks:', error); this.loading = false; } }); } } ``` ## Dark/Light Mode Requirements ThrillWiki requires comprehensive dark/light mode support across all frontend implementations. ### CSS Variables Approach ```css :root { /* Light mode colors */ --bg-primary: #ffffff; --bg-secondary: #f8f9fa; --text-primary: #212529; --text-secondary: #6c757d; --border-color: #dee2e6; --accent-color: #007bff; } [data-theme="dark"] { /* Dark mode colors */ --bg-primary: #1a1a1a; --bg-secondary: #2d2d2d; --text-primary: #ffffff; --text-secondary: #b0b0b0; --border-color: #404040; --accent-color: #4dabf7; } body { background-color: var(--bg-primary); color: var(--text-primary); transition: background-color 0.3s ease, color 0.3s ease; } ``` ### Theme Toggle Implementation **Vanilla JavaScript:** ```javascript class ThemeManager { constructor() { this.theme = localStorage.getItem('theme') || 'light'; this.applyTheme(); } applyTheme() { document.documentElement.setAttribute('data-theme', this.theme); localStorage.setItem('theme', this.theme); } toggle() { this.theme = this.theme === 'light' ? 'dark' : 'light'; this.applyTheme(); } setTheme(theme) { this.theme = theme; this.applyTheme(); } } const themeManager = new ThemeManager(); // Theme toggle button document.getElementById('theme-toggle').addEventListener('click', () => { themeManager.toggle(); }); ``` **React Hook:** ```jsx import { useState, useEffect } from 'react'; export const useTheme = () => { const [theme, setTheme] = useState(() => { return localStorage.getItem('theme') || 'light'; }); useEffect(() => { document.documentElement.setAttribute('data-theme', theme); localStorage.setItem('theme', theme); }, [theme]); const toggleTheme = () => { setTheme(prev => prev === 'light' ? 'dark' : 'light'); }; return { theme, toggleTheme, setTheme }; }; // Component usage const ThemeToggle = () => { const { theme, toggleTheme } = useTheme(); return ( ); }; ``` **Vue Composable:** ```javascript import { ref, watch } from 'vue'; export function useTheme() { const theme = ref(localStorage.getItem('theme') || 'light'); const applyTheme = (newTheme) => { document.documentElement.setAttribute('data-theme', newTheme); localStorage.setItem('theme', newTheme); }; const toggleTheme = () => { theme.value = theme.value === 'light' ? 'dark' : 'light'; }; const setTheme = (newTheme) => { theme.value = newTheme; }; watch(theme, applyTheme, { immediate: true }); return { theme: readonly(theme), toggleTheme, setTheme }; } ``` ### Tailwind CSS Integration If using Tailwind CSS, configure dark mode in `tailwind.config.js`: ```javascript module.exports = { darkMode: ['class', '[data-theme="dark"]'], theme: { extend: { colors: { primary: { light: '#ffffff', dark: '#1a1a1a' }, secondary: { light: '#f8f9fa', dark: '#2d2d2d' } } } } } ``` Usage in components: ```html
Content that adapts to theme
``` ## Error Handling ### HTTP Status Codes The API returns standard HTTP status codes: - **200 OK** - Successful GET, PUT, PATCH - **201 Created** - Successful POST - **204 No Content** - Successful DELETE - **400 Bad Request** - Invalid request data - **401 Unauthorized** - Authentication required - **403 Forbidden** - Permission denied - **404 Not Found** - Resource not found - **500 Internal Server Error** - Server error ### Error Response Format ```json { "error": "Error message", "details": { "field_name": ["Field-specific error message"] } } ``` ### Error Handling Examples **JavaScript:** ```javascript async function handleApiCall(apiFunction) { try { const response = await apiFunction(); return response; } catch (error) { if (error.response) { // Server responded with error status const { status, data } = error.response; switch (status) { case 401: // Handle authentication error redirectToLogin(); break; case 403: // Handle permission error showErrorMessage('You do not have permission to perform this action'); break; case 404: // Handle not found showErrorMessage('Resource not found'); break; case 500: // Handle server error showErrorMessage('Server error. Please try again later.'); break; default: // Handle other errors showErrorMessage(data.error || 'An error occurred'); } } else if (error.request) { // Network error showErrorMessage('Network error. Please check your connection.'); } else { // Other error showErrorMessage('An unexpected error occurred'); } throw error; } } ``` **React Error Boundary:** ```jsx class ApiErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('API Error:', error, errorInfo); } render() { if if (this.state.hasError) { return (

Something went wrong

Please try refreshing the page or contact support if the problem persists.

); } return this.props.children; } } ``` ## Best Practices ### Security Considerations 1. **Token Storage** - Use `httpOnly` cookies for production when possible - Avoid storing tokens in localStorage for sensitive applications - Implement token refresh mechanisms 2. **HTTPS Only** - Always use HTTPS in production - Configure secure cookie flags 3. **Input Validation** - Validate all user inputs on the frontend - Sanitize data before sending to API 4. **CORS Configuration** - Configure CORS properly in Django settings - Restrict allowed origins in production ### Performance Optimization 1. **Request Caching** ```javascript // Simple cache implementation class ApiCache { constructor(ttl = 300000) { // 5 minutes default this.cache = new Map(); this.ttl = ttl; } get(key) { const item = this.cache.get(key); if (!item) return null; if (Date.now() > item.expiry) { this.cache.delete(key); return null; } return item.data; } set(key, data) { this.cache.set(key, { data, expiry: Date.now() + this.ttl }); } clear() { this.cache.clear(); } } const apiCache = new ApiCache(); // Usage in API calls async function getCachedParks() { const cacheKey = 'parks_list'; let parks = apiCache.get(cacheKey); if (!parks) { const response = await api.get('/parks/'); parks = response.data; apiCache.set(cacheKey, parks); } return parks; } ``` 2. **Request Debouncing** ```javascript function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Usage for search const debouncedSearch = debounce(async (query) => { if (query.length > 2) { const results = await api.get('/parks/search/', { params: { q: query } }); setSearchResults(results.data); } }, 300); ``` 3. **Pagination Handling** ```javascript class PaginatedAPI { constructor(baseURL) { this.baseURL = baseURL; } async getAllPages(endpoint, params = {}) { let allResults = []; let nextUrl = `${this.baseURL}${endpoint}`; while (nextUrl) { const response = await fetch(nextUrl, { headers: this.getHeaders(), ...params }); const data = await response.json(); allResults = allResults.concat(data.results || []); nextUrl = data.next; } return allResults; } async getPage(endpoint, page = 1, pageSize = 20) { const response = await fetch(`${this.baseURL}${endpoint}?page=${page}&page_size=${pageSize}`, { headers: this.getHeaders() }); return response.json(); } } ``` ### Code Organization 1. **Service Layer Pattern** ```javascript // services/api.js export class APIService { constructor(baseURL, authService) { this.baseURL = baseURL; this.authService = authService; } async request(endpoint, options = {}) { // Common request logic } } // services/parksService.js export class ParksService extends APIService { async getParks(filters = {}) { return this.request('/parks/', { params: filters }); } async getPark(id) { return this.request(`/parks/${id}/`); } } // services/index.js export { APIService } from './api'; export { ParksService } from './parksService'; export { RidesService } from './ridesService'; export { AuthService } from './authService'; ``` 2. **Environment Configuration** ```javascript // config/api.js const config = { development: { API_BASE_URL: 'http://localhost:8000/api/v1', ENABLE_LOGGING: true, CACHE_TTL: 60000 // 1 minute }, production: { API_BASE_URL: 'https://api.thrillwiki.com/api/v1', ENABLE_LOGGING: false, CACHE_TTL: 300000 // 5 minutes } }; export default config[process.env.NODE_ENV || 'development']; ``` ## Troubleshooting ### Common Issues #### 1. CORS Errors **Problem:** Browser blocks requests due to CORS policy **Solution:** - Ensure Django CORS settings are configured: ```python # settings.py CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "http://127.0.0.1:3000", "http://localhost:5173", # Vite default ] CORS_ALLOW_CREDENTIALS = True ``` #### 2. Authentication Token Issues **Problem:** 401 Unauthorized errors **Solutions:** - Check token format: `Authorization: Token abc123def456` - Verify token is not expired - Ensure token is included in requests: ```javascript // Check if token exists const token = localStorage.getItem('thrillwiki_token'); if (!token) { // Redirect to login window.location.href = '/login'; return; } ``` #### 3. Network Connectivity **Problem:** Network errors or timeouts **Solutions:** - Implement retry logic: ```javascript async function retryRequest(fn, retries = 3, delay = 1000) { try { return await fn(); } catch (error) { if (retries > 0 && error.code === 'NETWORK_ERROR') { await new Promise(resolve => setTimeout(resolve, delay)); return retryRequest(fn, retries - 1, delay * 2); } throw error; } } ``` #### 4. Data Serialization Issues **Problem:** Unexpected data formats or missing fields **Solutions:** - Always validate API responses: ```javascript function validateParkData(park) { const required = ['id', 'name', 'location']; const missing = required.filter(field => !park[field]); if (missing.length > 0) { throw new Error(`Missing required fields: ${missing.join(', ')}`); } return park; } ``` #### 5. Proxy Configuration Issues (Vite/Webpack) **Problem:** API calls fail in development due to proxy misconfiguration **Vite Solution:** ```javascript // vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, secure: false, } } } }); ``` **Webpack Solution:** ```javascript // webpack.config.js or next.config.js module.exports = { async rewrites() { return [ { source: '/api/:path*', destination: 'http://localhost:8000/api/:path*', }, ]; }, }; ``` ### Debugging Tools #### 1. API Request Logging ```javascript class APILogger { static log(method, url, data, response) { if (process.env.NODE_ENV === 'development') { console.group(`🌐 API ${method.toUpperCase()} ${url}`); if (data) console.log('📤 Request:', data); console.log('📥 Response:', response); console.groupEnd(); } } static error(method, url, error) { console.group(`❌ API ${method.toUpperCase()} ${url} FAILED`); console.error('Error:', error); console.groupEnd(); } } // Usage in API service async request(endpoint, options = {}) { try { const response = await fetch(url, options); const data = await response.json(); APILogger.log(options.method || 'GET', endpoint, options.body, data); return data; } catch (error) { APILogger.error(options.method || 'GET', endpoint, error); throw error; } } ``` #### 2. Network Monitoring ```javascript // Monitor network status class NetworkMonitor { constructor() { this.isOnline = navigator.onLine; this.listeners = []; window.addEventListener('online', () => this.setOnline(true)); window.addEventListener('offline', () => this.setOnline(false)); } setOnline(status) { this.isOnline = status; this.listeners.forEach(listener => listener(status)); } onStatusChange(callback) { this.listeners.push(callback); } } const networkMonitor = new NetworkMonitor(); networkMonitor.onStatusChange((isOnline) => { if (isOnline) { console.log('🟢 Back online - retrying failed requests'); // Retry failed requests } else { console.log('🔴 Gone offline - queuing requests'); // Queue requests for later } }); ``` ### Testing API Integration #### Unit Testing Example (Jest) ```javascript // __tests__/api.test.js import { APIService } from '../services/api'; // Mock fetch global.fetch = jest.fn(); describe('APIService', () => { let apiService; beforeEach(() => { apiService = new APIService('http://localhost:8000/api/v1'); fetch.mockClear(); }); test('should fetch parks successfully', async () => { const mockParks = [ { id: 1, name: 'Test Park', location: 'Test Location' } ]; fetch.mockResolvedValueOnce({ ok: true, json: async () => ({ results: mockParks }) }); const parks = await apiService.getParks(); expect(fetch).toHaveBeenCalledWith( 'http://localhost:8000/api/v1/parks/', expect.objectContaining({ headers: expect.objectContaining({ 'Content-Type': 'application/json' }) }) ); expect(parks.results).toEqual(mockParks); }); test('should handle authentication errors', async () => { fetch.mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ error: 'Unauthorized' }) }); await expect(apiService.getParks()).rejects.toThrow('Unauthorized'); }); }); ``` ### Performance Monitoring ```javascript // Performance monitoring utility class PerformanceMonitor { static startTimer(label) { performance.mark(`${label}-start`); } static endTimer(label) { performance.mark(`${label}-end`); performance.measure(label, `${label}-start`, `${label}-end`); const measure = performance.getEntriesByName(label)[0]; console.log(`⏱️ ${label}: ${measure.duration.toFixed(2)}ms`); // Clean up performance.clearMarks(`${label}-start`); performance.clearMarks(`${label}-end`); performance.clearMeasures(label); } } // Usage PerformanceMonitor.startTimer('api-parks-fetch'); const parks = await api.getParks(); PerformanceMonitor.endTimer('api-parks-fetch'); ``` ## Additional Resources ### API Documentation - **Swagger/OpenAPI Docs**: `http://localhost:8000/api/docs/` - **ReDoc Documentation**: `http://localhost:8000/api/redoc/` - **API Schema**: `http://localhost:8000/api/schema/` ### Development Tools - **Django Admin**: `http://localhost:8000/admin/` - **Health Check**: `http://localhost:8000/api/v1/health/` - **Environment Settings**: `http://localhost:8000/env-settings/` ### Example Projects Check the `frontend/` directory for a complete Vue.js implementation example that demonstrates all the patterns described in this guide. ### Community Resources - **Django REST Framework Documentation**: https://www.django-rest-framework.org/ - **Vue.js Documentation**: https://vuejs.org/ - **React Documentation**: https://react.dev/ - **Angular Documentation**: https://angular.io/ --- This guide provides a comprehensive foundation for connecting any frontend framework to the ThrillWiki Django backend. For specific implementation questions or issues not covered here, please refer to the project's issue tracker or documentation.