tmdb-integration
TMDB Integration Skill
You are an expert in integrating The Movie Database (TMDB) API with React Native TV applications. This skill activates when users ask about:
- Fetching movie or TV show data
- Displaying poster and backdrop images
- Implementing search functionality
- Getting trending content
- Fetching video trailers
- TMDB authentication and API keys
- Rate limiting and optimization
- TypeScript types for TMDB responses
Authentication
TMDB offers two equivalent authentication methods:
API Key (Query Parameter)
const url = `https://api.themoviedb.org/3/movie/550?api_key=${API_KEY}`;
Bearer Token (Header) - Recommended
const headers = {
'Authorization': `Bearer ${ACCESS_TOKEN}`,
'Accept': 'application/json'
};
Both tokens are generated in your TMDB account settings. Bearer token is recommended for production as credentials aren't visible in URLs.
Image URL Construction
Base URL: https://image.tmdb.org/t/p/
Official Sizes (use these for CDN caching):
| Type | Available Sizes |
|---|---|
| Poster | w92, w154, w185, w342, w500, w780, original |
| Backdrop | w300, w780, w1280, original |
| Logo | w45, w92, w154, w185, w300, w500, original |
| Profile | w45, w185, h632, original |
Image URL Helper:
const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/';
type PosterSize = 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original';
type BackdropSize = 'w300' | 'w780' | 'w1280' | 'original';
export function getPosterUrl(path: string | null, size: PosterSize = 'w500'): string | null {
if (!path) return null;
return `${TMDB_IMAGE_BASE}${size}${path}`;
}
export function getBackdropUrl(path: string | null, size: BackdropSize = 'w1280'): string | null {
if (!path) return null;
return `${TMDB_IMAGE_BASE}${size}${path}`;
}
Important: Only use official sizes - non-standard sizes bypass CDN caching and are 10-50x slower.
Essential Endpoints
Trending Content
GET /trending/{media_type}/{time_window}
media_type: movie, tv, person, all
time_window: day, week
Discovery
GET /discover/movie
GET /discover/tv
Parameters:
- sort_by: popularity.desc, vote_average.desc, release_date.desc
- with_genres: 28,12 (AND) or 28|12 (OR)
- page: pagination (20 items per page)
Search
GET /search/movie?query={term}
GET /search/tv?query={term}
GET /search/multi?query={term} // Movies, TV, and people
Details with Related Data
GET /movie/{id}?append_to_response=videos,credits,images
GET /tv/{id}?append_to_response=videos,credits,images,season/1,season/2
append_to_response combines multiple requests into one (doesn't count toward rate limits).
Genres
GET /genre/movie/list
GET /genre/tv/list
TypeScript Interfaces
// Base types
export interface Movie {
id: number;
title: string;
overview: string;
poster_path: string | null;
backdrop_path: string | null;
release_date: string;
vote_average: number;
vote_count: number;
popularity: number;
genre_ids?: number[];
adult: boolean;
}
export interface TVShow {
id: number;
name: string;
overview: string;
poster_path: string | null;
backdrop_path: string | null;
first_air_date: string;
vote_average: number;
vote_count: number;
popularity: number;
genre_ids?: number[];
origin_country: string[];
}
export interface TMDBResponse<T> {
page: number;
results: T[];
total_pages: number;
total_results: number;
}
// Detail types
export interface MovieDetails extends Movie {
budget: number;
revenue: number;
runtime: number;
status: string;
tagline: string;
genres: Genre[];
production_companies: ProductionCompany[];
credits?: Credits;
videos?: { results: Video[] };
images?: Images;
}
export interface TVDetails extends TVShow {
number_of_episodes: number;
number_of_seasons: number;
episode_run_time: number[];
seasons: Season[];
networks: Network[];
status: string;
credits?: Credits;
videos?: { results: Video[] };
}
export interface Genre {
id: number;
name: string;
}
export interface Video {
id: string;
key: string; // YouTube/Vimeo video ID
name: string;
site: 'YouTube' | 'Vimeo';
size: number;
type: 'Trailer' | 'Teaser' | 'Clip' | 'Featurette' | 'Behind the Scenes';
official: boolean;
published_at: string;
}
export interface Credits {
cast: CastMember[];
crew: CrewMember[];
}
export interface CastMember {
id: number;
name: string;
character: string;
profile_path: string | null;
order: number;
}
export interface CrewMember {
id: number;
name: string;
job: string;
department: string;
profile_path: string | null;
}
export interface Season {
id: number;
season_number: number;
name: string;
overview: string;
air_date: string;
episode_count: number;
poster_path: string | null;
}
export interface Episode {
id: number;
name: string;
overview: string;
episode_number: number;
season_number: number;
still_path: string | null;
air_date: string;
runtime: number;
vote_average: number;
}
Axios Client Setup
import axios from 'axios';
const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
const tmdbClient = axios.create({
baseURL: TMDB_BASE_URL,
timeout: 10000,
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${process.env.TMDB_ACCESS_TOKEN}`,
},
});
// Add default language
tmdbClient.interceptors.request.use((config) => {
config.params = {
...config.params,
language: 'en-US',
};
return config;
});
// Error handling
tmdbClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 429) {
// Rate limited - implement retry with backoff
console.warn('TMDB rate limit hit');
}
return Promise.reject(error);
}
);
export default tmdbClient;
React Native Hooks
useTrending Hook
import { useState, useEffect } from 'react';
import tmdbClient from '../services/tmdbClient';
import { Movie, TVShow, TMDBResponse } from '../types/tmdb';
type MediaType = 'movie' | 'tv' | 'all';
type TimeWindow = 'day' | 'week';
export function useTrending<T extends Movie | TVShow>(
mediaType: MediaType = 'movie',
timeWindow: TimeWindow = 'week'
) {
const [data, setData] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchTrending() {
try {
setLoading(true);
const response = await tmdbClient.get<TMDBResponse<T>>(
`/trending/${mediaType}/${timeWindow}`
);
if (!cancelled) {
setData(response.data.results);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchTrending();
return () => { cancelled = true; };
}, [mediaType, timeWindow]);
return { data, loading, error };
}
useMovieDetails Hook
export function useMovieDetails(movieId: number) {
const [movie, setMovie] = useState<MovieDetails | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchDetails() {
try {
setLoading(true);
const response = await tmdbClient.get<MovieDetails>(
`/movie/${movieId}`,
{
params: {
append_to_response: 'videos,credits,images',
},
}
);
if (!cancelled) {
setMovie(response.data);
}
} catch (err) {
if (!cancelled) {
setError(err as Error);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
if (movieId) {
fetchDetails();
}
return () => { cancelled = true; };
}, [movieId]);
return { movie, loading, error };
}
useSearch Hook with Debounce
import { useState, useCallback, useRef } from 'react';
import { debounce } from 'lodash';
export function useSearch() {
const [results, setResults] = useState<(Movie | TVShow)[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const searchRef = useRef(
debounce(async (query: string) => {
if (!query.trim()) {
setResults([]);
return;
}
try {
setLoading(true);
const response = await tmdbClient.get('/search/multi', {
params: { query },
});
setResults(response.data.results.filter(
(item: any) => item.media_type === 'movie' || item.media_type === 'tv'
));
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
}, 300)
);
const search = useCallback((query: string) => {
searchRef.current(query);
}, []);
return { results, loading, error, search };
}
Rate Limiting
Current Limits:
- 50 requests per second
- 20 simultaneous connections per IP
Optimization Strategies:
- Use append_to_response - Combine requests (free, no rate limit impact)
- Implement caching - Cache responses with TTL
- Debounce searches - Wait 300ms after user stops typing
- Batch requests - Group API calls with small delays
Common Pitfalls & Solutions
| Pitfall | Solution |
|---|---|
| API key in client-side code | Use backend proxy in production |
| Slow image loading | Only use official sizes (w342, w500, w780) |
| Missing images crash app | Always check for null: poster_path && getPosterUrl(poster_path) |
| Wrong video displayed | Filter: videos.filter(v => v.type === 'Trailer' && v.official) |
| Rate limit errors | Implement exponential backoff, use append_to_response |
| State update on unmounted component | Use cleanup flag in useEffect |
| Search fires too often | Debounce search input (300-500ms) |
| Can't get all TV episodes | Use append_to_response=season/1,season/2,... (max 20) |
Error Codes
| Code | Meaning | Action |
|---|---|---|
| 7 | Invalid API key | Check for typos, verify key in settings |
| 10 | Suspended API key | Contact TMDB support |
| 34 | Resource not found | May be temporary - retry once |
| 429 | Rate limit exceeded | Implement backoff, reduce request rate |
Video URL Construction
function getVideoUrl(video: Video): string {
if (video.site === 'YouTube') {
return `https://www.youtube.com/watch?v=${video.key}`;
}
if (video.site === 'Vimeo') {
return `https://vimeo.com/${video.key}`;
}
return '';
}
// Get official trailer
function getOfficialTrailer(videos: Video[]): Video | undefined {
return videos.find(v => v.type === 'Trailer' && v.official)
|| videos.find(v => v.type === 'Trailer')
|| videos[0];
}
Resources
More from giolaq/multi-tv-dev-power
rn-to-tv-quickstart
Quick start guide for experienced React Native developers transitioning to TV app development. Covers Fire OS, Android TV, Apple TV (tvOS), Vega OS, and Web TV platforms. Focuses on what's different from mobile: spatial navigation, remote controls, focus management, and 10-foot UI patterns. Skip the basics - get your first TV app running fast.
10multi-tv-builder
Complete knowledge base for building production-ready cross-platform TV applications with React Native - supporting Android TV, Apple TV, Fire TV (Fire OS & Vega OS), and Web TV platforms with a single codebase. Use when implementing TV apps, monorepo architecture, spatial navigation, video playback, platform-specific optimizations, or migrating from Vega to multi-platform.
2