18 KiB
Frontend-Backend Integration Guide
Overview
This guide explains how to properly connect EasyStream's frontend JavaScript code with the backend API endpoints. It covers migrating from legacy jQuery AJAX calls to modern fetch API using the provided api-helper.js client.
Table of Contents
- Architecture Overview
- Migration Strategy
- Using api-helper.js
- Authentication Patterns
- Common Migration Patterns
- Error Handling
- Testing Integration
Architecture Overview
Current State
EasyStream has three layers of API integration:
-
Modern API Endpoints (
/api/*.php)- RESTful design
- JSON request/response
- JWT + Session auth support
- Proper error handling
-
Modern Frontend Client (
api-helper.js)- Fetch API based
- Promise/async-await pattern
- Automatic token management
- Error handling helpers
-
Legacy Frontend Code (jQuery AJAX calls)
- Scattered across multiple files
- Inconsistent patterns
- Needs migration
Target State
All frontend code should use api-helper.js for consistency, maintainability, and better error handling.
Migration Strategy
Phase 1: Identify Legacy Code (COMPLETED)
Found legacy jQuery AJAX calls in:
f_scripts/fe/js/browse.init.jsf_scripts/fe/js/jquery.init.js- Various other frontend scripts
Phase 2: Create Modern API Endpoints (COMPLETED)
Created comprehensive RESTful APIs:
/api/videos.php- Video operations/api/user.php- User profile operations/api/comments.php- Comment operations/api/subscriptions.php- Subscription operations/api/auth.php- Authentication (already existed, enhanced)
Phase 3: Enhance Frontend Client (COMPLETED)
Enhanced api-helper.js with methods for all endpoints.
Phase 4: Migrate Legacy Code (IN PROGRESS)
Replace jQuery AJAX calls with modern fetch API using api-helper.js.
Phase 5: Test & Validate (PENDING)
Test all integrated endpoints end-to-end.
Using api-helper.js
Basic Setup
The API helper is automatically initialized on page load:
// Available globally
const api = window.api;
// Or create new instance
const customAPI = new EasyStreamAPI('/api');
Authentication
Login
// Login and store token
try {
const result = await api.login('username', 'password');
if (result.success) {
console.log('Logged in as:', result.user.usr_user);
// Token is automatically stored in localStorage
}
} catch (error) {
console.error('Login failed:', error.message);
}
Check Authentication
if (api.isAuthenticated()) {
console.log('User is logged in');
} else {
console.log('User needs to login');
}
Logout
await api.logout();
// Token is automatically cleared
Making API Calls
Videos
// List videos
const videos = await api.getVideos({
page: 1,
limit: 20,
sort: 'popular',
category: 'entertainment'
});
// Get single video
const video = await api.getVideo('123456');
// Search videos
const searchResults = await api.searchVideos('search query', { page: 1 });
// Create video
const newVideo = await api.createVideo({
title: 'My Video',
description: 'Video description',
privacy: 'public'
});
// Update video
await api.updateVideo('123456', {
file_title: 'Updated Title'
});
// Delete video
await api.deleteVideo('123456');
// Like video
await api.likeVideo('123456', 'like');
// Record view
await api.recordVideoView('123456');
// Watch later
await api.toggleWatchLater('123456');
User Profile
// Get current user's profile
const myProfile = await api.getMyProfile();
// Get another user's profile
const userProfile = await api.getUserProfile(123);
// Update profile
await api.updateProfile({
usr_dname: 'New Name',
usr_about: 'Updated bio'
});
// Upload avatar
const fileInput = document.getElementById('avatar-input');
const file = fileInput.files[0];
await api.uploadAvatar(file);
// Get user stats
const stats = await api.getUserStats();
// Get user's videos
const userVideos = await api.getUserVideos(123, { page: 1 });
Comments
// Get comments
const comments = await api.getComments('123456', {
page: 1,
sort: 'recent'
});
// Create comment
const newComment = await api.createComment(
'123456', // file_key
'This is my comment',
null // parent_id (null for top-level comment)
);
// Reply to comment
const reply = await api.createComment(
'123456',
'This is a reply',
456 // parent comment ID
);
// Update comment
await api.updateComment(789, 'Updated text');
// Delete comment
await api.deleteComment(789);
// Like comment
await api.likeComment(789);
// Report comment
await api.reportComment(789, 'Spam');
Subscriptions
// Get subscriptions
const subs = await api.getSubscriptions();
// Check if subscribed
const status = await api.checkSubscription(123);
if (status.data.is_subscribed) {
console.log('Already subscribed');
}
// Subscribe
await api.subscribe(123);
// Unsubscribe
await api.unsubscribe(123);
// Get subscription feed
const feed = await api.getSubscriptionFeed({ page: 1 });
// Get subscribers
const subscribers = await api.getSubscribers(123);
Authentication Patterns
JWT Token Authentication (Recommended)
Best for: SPAs, mobile apps, API clients
// 1. Login to get token
const result = await api.login('username', 'password');
// 2. Token is automatically stored and used for subsequent requests
const profile = await api.getMyProfile();
// 3. Logout clears token
await api.logout();
Session-based Authentication
Best for: Traditional multi-page websites
// Login via form submission (handles CSRF automatically)
const formData = new FormData(loginForm);
const response = await fetch('/api/auth.php?action=login', {
method: 'POST',
body: formData,
credentials: 'include' // Important for cookies
});
Hybrid Approach
Use sessions for page navigation, JWT for API calls:
// Check if user has session
const statusResponse = await fetch('/api/auth.php?action=status', {
credentials: 'include'
});
const status = await statusResponse.json();
if (status.authenticated) {
// User has session, optionally get JWT for API calls
// (if needed for cross-origin requests)
}
Common Migration Patterns
Pattern 1: Simple GET Request
Before (jQuery):
jQuery.get(url, function(result) {
console.log(result);
updateUI(result);
});
After (api-helper):
try {
const result = await api.get(url);
console.log(result);
updateUI(result.data);
} catch (error) {
api.handleError(error);
}
Pattern 2: POST with Data
Before (jQuery):
jQuery.post(url, {
field1: value1,
field2: value2
}, function(result) {
if (result.success) {
showSuccess('Saved!');
}
});
After (api-helper):
try {
const result = await api.post(url, {
field1: value1,
field2: value2
});
if (result.success) {
showSuccess('Saved!');
}
} catch (error) {
api.handleError(error);
}
Pattern 3: Load More / Pagination
Before (jQuery):
jQuery(".more-button").click(function() {
var page = parseInt(jQuery(this).attr("rel-page"));
var url = _rel + "?p=0&m=" + idnr + "&sort=" + type + "&page=" + page;
jQuery.get(url, function(result) {
jQuery("#list ul").append(result);
jQuery(".more-button").attr("rel-page", page + 1);
});
});
After (api-helper):
document.addEventListener('click', async (e) => {
if (e.target.classList.contains('more-button')) {
const page = parseInt(e.target.getAttribute('data-page'));
try {
const result = await api.getVideos({
page: page,
sort: currentSort,
category: currentCategory
});
if (result.success) {
appendVideos(result.data.videos);
e.target.setAttribute('data-page', page + 1);
// Hide button if no more pages
if (page >= result.data.pagination.pages) {
e.target.style.display = 'none';
}
}
} catch (error) {
api.handleError(error);
}
}
});
function appendVideos(videos) {
const list = document.getElementById('video-list');
videos.forEach(video => {
const item = createVideoElement(video);
list.appendChild(item);
});
}
Pattern 4: Form Submission
Before (jQuery):
jQuery("#comment-form").submit(function(e) {
e.preventDefault();
var formData = jQuery(this).serialize();
jQuery.post("/submit-comment.php", formData, function(result) {
if (result.success) {
addCommentToUI(result.comment);
jQuery("#comment-form")[0].reset();
}
});
});
After (api-helper):
document.getElementById('comment-form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const commentText = formData.get('comment_text');
const fileKey = formData.get('file_key');
try {
const result = await api.createComment(fileKey, commentText);
if (result.success) {
addCommentToUI(result.data);
e.target.reset();
}
} catch (error) {
api.handleError(error);
}
});
Pattern 5: Watch Later Toggle
Before (jQuery):
jQuery(".watch_later_wrap").click(function() {
var file_key = jQuery(this).attr("rel-key");
var file_type = jQuery(this).attr("rel-type");
var url = _rel + "?a=cb-watchadd&for=sort-" + file_type;
var _this = jQuery(this);
jQuery.post(url, {"fileid[0]": file_key}, function(result) {
_this.find(".icon-clock")
.removeClass("icon-clock")
.addClass("icon-check");
_this.next().text("In Watch List");
});
});
After (api-helper):
document.addEventListener('click', async (e) => {
const watchBtn = e.target.closest('.watch-later-btn');
if (!watchBtn) return;
const fileKey = watchBtn.dataset.fileKey;
try {
const result = await api.toggleWatchLater(fileKey);
if (result.success) {
const icon = watchBtn.querySelector('.icon');
const text = watchBtn.querySelector('.text');
if (result.data.action === 'added') {
icon.classList.remove('icon-clock');
icon.classList.add('icon-check');
text.textContent = 'In Watch List';
} else {
icon.classList.remove('icon-check');
icon.classList.add('icon-clock');
text.textContent = 'Watch Later';
}
}
} catch (error) {
api.handleError(error);
}
});
Pattern 6: Subscribe Button
Before (jQuery):
jQuery(".subscribe-btn").click(function() {
var channelId = jQuery(this).data("channel-id");
jQuery.post("/subscribe.php", { channel_id: channelId }, function(result) {
if (result.success) {
jQuery(".subscribe-btn").text("Subscribed");
jQuery(".subscriber-count").text(result.subscriber_count);
}
});
});
After (api-helper):
document.addEventListener('click', async (e) => {
const subscribeBtn = e.target.closest('.subscribe-btn');
if (!subscribeBtn) return;
const channelId = parseInt(subscribeBtn.dataset.channelId);
const isSubscribed = subscribeBtn.classList.contains('subscribed');
try {
if (isSubscribed) {
await api.unsubscribe(channelId);
subscribeBtn.classList.remove('subscribed');
subscribeBtn.textContent = 'Subscribe';
} else {
const result = await api.subscribe(channelId);
subscribeBtn.classList.add('subscribed');
subscribeBtn.textContent = 'Subscribed';
// Update subscriber count if available
if (result.data.subscriber_count) {
document.querySelector('.subscriber-count').textContent =
result.data.subscriber_count;
}
}
} catch (error) {
api.handleError(error);
}
});
Error Handling
Comprehensive Error Handling
async function performAction() {
try {
// Show loading state
showLoading();
const result = await api.someAction();
if (result.success) {
showSuccess('Action completed!');
updateUI(result.data);
} else {
showError(result.error || 'Action failed');
}
} catch (error) {
// Handle different error types
if (error.message.includes('Authentication')) {
// Redirect to login
window.location.href = '/signin';
} else if (error.message.includes('Network')) {
showError('Network error. Please check your connection.');
} else {
showError(error.message || 'An unexpected error occurred');
}
// Log error for debugging
console.error('Action failed:', error);
} finally {
// Always hide loading state
hideLoading();
}
}
Using Built-in Error Handler
try {
const result = await api.someAction();
// Handle success
} catch (error) {
// Use built-in error handler with custom callback
api.handleError(error, (message) => {
showNotification('error', message);
});
}
Retry Logic
async function apiCallWithRetry(apiMethod, maxRetries = 3) {
let lastError;
for (let i = 0; i < maxRetries; i++) {
try {
return await apiMethod();
} catch (error) {
lastError = error;
// Don't retry on auth errors
if (error.message.includes('Authentication')) {
throw error;
}
// Wait before retry (exponential backoff)
if (i < maxRetries - 1) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, i) * 1000));
}
}
}
throw lastError;
}
// Usage
try {
const result = await apiCallWithRetry(() => api.getVideos({ page: 1 }));
} catch (error) {
api.handleError(error);
}
Testing Integration
Manual Testing Checklist
-
Authentication
- Login with username
- Login with email
- Invalid credentials show error
- Token persists after page reload
- Logout clears token
- Expired token triggers re-login
-
Videos
- List videos loads correctly
- Pagination works
- Sorting works (popular, recent, etc.)
- Category filtering works
- Single video loads with all details
- Search returns relevant results
- Like/dislike updates count
- View count increments
- Watch later toggle works
-
User Profile
- Profile loads correctly
- Profile update saves changes
- Avatar upload works
- Statistics display correctly
- User's videos load
-
Comments
- Comments load for video
- Create comment works
- Reply to comment works
- Edit comment works
- Delete comment works
- Like comment updates count
- Pagination works
-
Subscriptions
- Subscribe button works
- Unsubscribe works
- Subscription list displays
- Subscriber count updates
- Subscription feed loads
Automated Testing
Create test files in tests/integration/:
// Example: test-videos-api.js
describe('Videos API Integration', () => {
let api;
let testVideoId;
beforeAll(async () => {
api = new EasyStreamAPI('/api');
await api.login('testuser', 'testpass');
});
test('should list videos', async () => {
const result = await api.getVideos({ page: 1 });
expect(result.success).toBe(true);
expect(result.data.videos).toBeInstanceOf(Array);
});
test('should create video', async () => {
const result = await api.createVideo({
title: 'Test Video',
description: 'Test description'
});
expect(result.success).toBe(true);
testVideoId = result.data.file_key;
});
test('should get single video', async () => {
const result = await api.getVideo(testVideoId);
expect(result.success).toBe(true);
expect(result.data.file_key).toBe(testVideoId);
});
afterAll(async () => {
if (testVideoId) {
await api.deleteVideo(testVideoId);
}
await api.logout();
});
});
Browser Console Testing
Quick tests in browser console:
// Test login
await api.login('username', 'password')
// Test get videos
await api.getVideos({ page: 1 })
// Test create comment
await api.createComment('123456', 'Test comment')
// Check authentication
api.isAuthenticated()
// View stored token
localStorage.getItem('jwt_token')
Best Practices
- Always use try-catch for async API calls
- Show loading states during API requests
- Provide user feedback for success/error
- Cache results when appropriate
- Debounce search/autocomplete requests
- Validate input before sending to API
- Handle edge cases (empty results, network errors, etc.)
- Log errors for debugging
- Use TypeScript for better type safety (optional)
- Test thoroughly before deploying
Migration Priority
Migrate in this order:
-
Critical User Actions
- Login/Logout
- Video playback
- Comments
- Subscriptions
-
Content Display
- Video listings
- User profiles
- Search
-
Secondary Features
- Notifications
- Watch later
- Playlists
-
Admin Features
- Analytics
- Moderation
- Settings
Support
If you encounter issues during migration:
- Check API_DOCUMENTATION.md for endpoint details
- Review BACKEND_FRONTEND_INTEGRATION_FIXES.md
- Test endpoints using browser DevTools Network tab
- Check backend logs for errors
- Verify CORS configuration if cross-origin issues occur
Last Updated: January 2025