37 KiB
ThrillWiki Frontend Connection Guide
A comprehensive guide for connecting any frontend framework to the ThrillWiki Django REST API backend.
Table of Contents
- Overview
- API Architecture
- Authentication System
- Core API Endpoints
- Frontend Framework Examples
- Dark/Light Mode Requirements
- Error Handling
- Best Practices
- 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
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:
# 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
- Login/Signup - Obtain authentication token
- Store Token - Save token securely (localStorage, sessionStorage, or cookies)
- Include Token - Add token to all authenticated requests
- 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:
{
"username": "thrillseeker",
"password": "securepassword123"
}
Login Response:
{
"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:
{
"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
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
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 (
<AuthContext.Provider value={{ user, login, signup, logout, loading }}>
{children}
</AuthContext.Provider>
);
};
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 <div>Loading parks...</div>;
return (
<div>
<h2>Theme Parks</h2>
{parks.map(park => (
<div key={park.id} className="park-card">
<h3>{park.name}</h3>
<p>{park.description}</p>
<p>Location: {park.location}</p>
</div>
))}
</div>
);
};
Vue.js 3 with Composition API
<template>
<div>
<h2>Theme Parks</h2>
<div v-if="loading">Loading parks...</div>
<div v-else>
<div v-for="park in parks" :key="park.id" class="park-card">
<h3>{{ park.name }}</h3>
<p>{{ park.description }}</p>
<p>Location: {{ park.location }}</p>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useApi } from '@/composables/useApi'
const { apiService } = useApi()
const parks = ref([])
const loading = ref(true)
onMounted(async () => {
try {
const response = await apiService.getParks()
parks.value = response.data.results || response.data
} catch (error) {
console.error('Error fetching parks:', error)
} finally {
loading.value = false
}
})
</script>
Vue Composable (composables/useApi.js):
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
// 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<string | null>(
localStorage.getItem('thrillwiki_token')
);
private userSubject = new BehaviorSubject<User | null>(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<LoginResponse> {
return this.http.post<LoginResponse>(`${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<LoginResponse> {
return this.http.post<LoginResponse>(`${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<any> {
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<User> {
return this.http.get<User>(`${this.baseURL}/auth/current-user/`, {
headers: this.getHeaders()
}).pipe(
tap(user => this.userSubject.next(user))
);
}
// Parks methods
getParks(params?: any): Observable<any> {
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<any> {
return this.http.get(`${this.baseURL}/parks/${id}/`, {
headers: this.getHeaders()
});
}
createPark(parkData: any): Observable<any> {
return this.http.post(`${this.baseURL}/parks/`, parkData, {
headers: this.getHeaders()
});
}
// Rides methods
getRides(params?: any): Observable<any> {
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<any> {
return this.http.get(`${this.baseURL}/rides/${id}/`, {
headers: this.getHeaders()
});
}
createRide(rideData: any): Observable<any> {
return this.http.post(`${this.baseURL}/rides/`, rideData, {
headers: this.getHeaders()
});
}
}
Component Example:
// parks-list.component.ts
import { Component, OnInit } from '@angular/core';
import { ApiService } from '../services/api.service';
@Component({
selector: 'app-parks-list',
template: `
<div>
<h2>Theme Parks</h2>
<div *ngIf="loading">Loading parks...</div>
<div *ngIf="!loading">
<div *ngFor="let park of parks" class="park-card">
<h3>{{ park.name }}</h3>
<p>{{ park.description }}</p>
<p>Location: {{ park.location }}</p>
</div>
</div>
</div>
`
})
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
: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:
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:
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 (
<button onClick={toggleTheme} className="theme-toggle">
{theme === 'light' ? '🌙' : '☀️'}
</button>
);
};
Vue Composable:
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:
module.exports = {
darkMode: ['class', '[data-theme="dark"]'],
theme: {
extend: {
colors: {
primary: {
light: '#ffffff',
dark: '#1a1a1a'
},
secondary: {
light: '#f8f9fa',
dark: '#2d2d2d'
}
}
}
}
}
Usage in components:
<div class="bg-primary-light dark:bg-primary-dark text-gray-900 dark:text-white">
Content that adapts to theme
</div>
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
{
"error": "Error message",
"details": {
"field_name": ["Field-specific error message"]
}
}
Error Handling Examples
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:
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 (
<div className="error-boundary">
<h2>Something went wrong</h2>
<p>Please try refreshing the page or contact support if the problem persists.</p>
</div>
);
}
return this.props.children;
}
}
Best Practices
Security Considerations
-
Token Storage
- Use
httpOnlycookies for production when possible - Avoid storing tokens in localStorage for sensitive applications
- Implement token refresh mechanisms
- Use
-
HTTPS Only
- Always use HTTPS in production
- Configure secure cookie flags
-
Input Validation
- Validate all user inputs on the frontend
- Sanitize data before sending to API
-
CORS Configuration
- Configure CORS properly in Django settings
- Restrict allowed origins in production
Performance Optimization
-
Request Caching
// 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; } -
Request Debouncing
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); -
Pagination Handling
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
-
Service Layer Pattern
// 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'; -
Environment Configuration
// 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:
# 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:
// 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:
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:
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:
// vite.config.js
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
secure: false,
}
}
}
});
Webpack Solution:
// 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
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
// 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)
// __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
// 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.