# 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
Theme Parks
Loading parks...
{{ park.name }}
{{ park.description }}
Location: {{ park.location }}
```
**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.