Aller au contenu principal

Frontend Integration Guide

Guide for frontend developers integrating with the MyTV API.

API Overview

EnvironmentBase URL
Localhttp://localhost:3000/api/v2
Staginghttps://staging-api.mytelevision.app/api/v2
Productionhttps://api.mytelevision.app/api/v2
Swagger Docs{baseUrl}/api/docs

Authentication Flows

Standard Login

const response = await fetch('/api/v2/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: '[email protected]',
password: 'SecurePass123',
deviceName: 'Chrome Browser',
deviceType: 'web',
}),
});

const { tokens, user } = await response.json();
// tokens.accessToken - 1h validity
// tokens.refreshToken - 7d validity

Social Login (Firebase)

import { signInWithPopup, GoogleAuthProvider } from 'firebase/auth';

// 1. Get Firebase token
const result = await signInWithPopup(auth, new GoogleAuthProvider());
const firebaseToken = await result.user.getIdToken();

// 2. Exchange for API token
const response = await fetch('/api/v2/auth/social/google', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ idToken: firebaseToken }),
});

const { tokens } = await response.json();

Account System (Netflix-style)

// Step 1: Login to get account access
const loginResponse = await fetch('/api/v2/account-auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const { profiles } = await loginResponse.json();

// Step 2: Select a profile
const profileResponse = await fetch('/api/v2/account-auth/select-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accountToken}`,
},
body: JSON.stringify({ profileId: profiles[0].id }),
});
const { accessToken, refreshToken } = await profileResponse.json();

HTTP Client Setup

Axios with Token Refresh

import axios from 'axios';

const api = axios.create({
baseURL: process.env.API_URL,
headers: { 'Content-Type': 'application/json' },
});

// Request interceptor
api.interceptors.request.use((config) => {
const token = getAccessToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});

// Response interceptor with auto-refresh
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 401 && !error.config._retry) {
error.config._retry = true;

try {
const refreshToken = getRefreshToken();
const { data } = await axios.post(
`${process.env.API_URL}/auth/refresh`,
{
refreshToken,
},
);

setAccessToken(data.accessToken);
setRefreshToken(data.refreshToken);
error.config.headers.Authorization = `Bearer ${data.accessToken}`;
return api(error.config);
} catch (refreshError) {
logout();
return Promise.reject(refreshError);
}
}
return Promise.reject(error);
},
);

Internationalization (i18n)

The API supports fr_FR (default) and en_US.

// Via query parameter
const movies = await api.get('/movies?lang=en_US');

// Via Accept-Language header
const movies = await api.get('/movies', {
headers: { 'Accept-Language': 'en-US' },
});

Pagination

Standard Pagination

interface PaginatedResponse<T> {
data: T[];
meta: {
total: number;
page: number;
limit: number;
totalPages: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
}

const { data: movies, meta } = await api.get('/movies', {
params: { page: 1, limit: 20, status: 'PUBLISHED' },
});

Infinite Scroll with React Query

import { useInfiniteQuery } from '@tanstack/react-query';

function useMovies() {
return useInfiniteQuery({
queryKey: ['movies'],
queryFn: ({ pageParam = 1 }) =>
api.get(`/movies?page=${pageParam}&limit=20`).then((res) => res.data),
getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.page + 1 : undefined,
});
}

Streaming Integration

Video Player Setup (video.js + HLS)

// 1. Generate streaming token
const { data: stream } = await api.post('/streaming/generate-token', {
mediaType: 'MOVIE',
mediaId: movieId,
quality: 'HD_720P',
});

// 2. Initialize player
import videojs from 'video.js';

const player = videojs('video-element', {
sources: [
{
src: stream.streamUrl,
type: 'application/x-mpegURL',
},
],
html5: {
hls: {
overrideNative: true,
withCredentials: true,
},
},
});

User Engagement

Reactions (Like/Dislike)

// Toggle like
await api.post('/reactions', {
mediaType: 'MOVIE',
mediaId: movieId,
reactionType: 'LIKE',
});

// Get reaction status
const { data } = await api.get(
`/reactions/status?mediaType=MOVIE&mediaId=${movieId}`,
);

Favorites

// Add to favorites
await api.post('/favorites', { mediaType: 'MOVIE', mediaId: movieId });

// List favorites
const { data } = await api.get('/favorites?mediaType=MOVIE');

Watch History

// Update watch progress
await api.post('/watch-history', {
mediaType: 'MOVIE',
mediaId: movieId,
watchedDuration: 3600, // seconds
totalDuration: 7200,
});

// Get continue watching
const { data } = await api.get('/watch-history/continue-watching');

Error Handling

api.interceptors.response.use(
(response) => response,
(error) => {
const status = error.response?.status;
const message = error.response?.data?.message;

switch (status) {
case 400:
// Validation error - message is array
showValidationErrors(message);
break;
case 401:
// Token expired or invalid
redirectToLogin();
break;
case 403:
// Insufficient permissions
showForbiddenError();
break;
case 404:
showNotFoundError();
break;
case 429:
// Rate limited - retry with backoff
retryWithBackoff(error.config);
break;
default:
showGenericError();
}

return Promise.reject(error);
},
);

TypeScript Types

// Core types
interface Movie {
id: string;
title: string;
slug: string;
overview: string;
posterUrl: string;
releaseDate: string;
runtime: number;
voteAverage: number;
status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' | 'COMING_SOON';
accessType: 'FREE' | 'PREMIUM' | 'SUBSCRIPTION';
genres: Genre[];
}

interface Series {
id: string;
title: string;
slug: string;
overview: string;
posterUrl: string;
totalSeasons: number;
status: string;
accessType: string;
seasons: Season[];
}

interface Season {
id: string;
seasonNumber: number;
title: string;
episodes: Episode[];
}

interface Episode {
id: string;
episodeNumber: number;
title: string;
overview: string;
runtime: number;
}

interface User {
id: string;
email: string;
username: string;
firstName: string;
lastName: string;
role: 'USER' | 'MODERATOR' | 'ADMIN' | 'SUPER_ADMIN';
status: string;
}

interface Profile {
id: string;
name: string;
avatarUrl: string;
type: 'STANDARD' | 'KIDS';
isDefault: boolean;
}

interface AuthResponse {
user: User;
tokens: {
accessToken: string;
refreshToken: string;
tokenType: string;
expiresIn: number;
};
}

Best Practices

Token Storage

  • Never use localStorage for tokens (XSS vulnerable)
  • Use memory storage or httpOnly cookies
  • Implement auto-refresh before expiration

Request Caching

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 10 * 60 * 1000, // 10 minutes
},
},
});

Optimistic Updates

const mutation = useMutation({
mutationFn: (movieId: string) =>
api.post('/favorites', { mediaType: 'MOVIE', mediaId: movieId }),
onMutate: async (movieId) => {
await queryClient.cancelQueries(['favorites']);
const previousFavorites = queryClient.getQueryData(['favorites']);
queryClient.setQueryData(['favorites'], (old) => [...old, movieId]);
return { previousFavorites };
},
onError: (err, movieId, context) => {
queryClient.setQueryData(['favorites'], context.previousFavorites);
},
});

Testing with MSW

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
rest.get('/api/v2/movies', (req, res, ctx) => {
return res(
ctx.json({
data: [{ id: '1', title: 'Test Movie' }],
meta: { total: 1, page: 1, limit: 20, totalPages: 1 },
}),
);
}),
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());