Files
thrillwiki_django_no_react/docs/frontend-connection-guide.md

37 KiB

ThrillWiki Frontend Connection Guide

A comprehensive guide for connecting any frontend framework to the ThrillWiki Django REST API backend.

Table of Contents

  1. Overview
  2. API Architecture
  3. Authentication System
  4. Core API Endpoints
  5. Frontend Framework Examples
  6. Dark/Light Mode Requirements
  7. Error Handling
  8. Best Practices
  9. 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

  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:

{
  "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

  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

    // 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

    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

    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

    // 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

    // 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


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.