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)
typescript
1const url = `https://api.themoviedb.org/3/movie/550?api_key=${API_KEY}`;
typescript
1const headers = {
2 'Authorization': `Bearer ${ACCESS_TOKEN}`,
3 'Accept': 'application/json'
4};
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:
typescript
1const TMDB_IMAGE_BASE = 'https://image.tmdb.org/t/p/';
2
3type PosterSize = 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original';
4type BackdropSize = 'w300' | 'w780' | 'w1280' | 'original';
5
6export function getPosterUrl(path: string | null, size: PosterSize = 'w500'): string | null {
7 if (!path) return null;
8 return `${TMDB_IMAGE_BASE}${size}${path}`;
9}
10
11export function getBackdropUrl(path: string | null, size: BackdropSize = 'w1280'): string | null {
12 if (!path) return null;
13 return `${TMDB_IMAGE_BASE}${size}${path}`;
14}
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
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
typescript
1// Base types
2export interface Movie {
3 id: number;
4 title: string;
5 overview: string;
6 poster_path: string | null;
7 backdrop_path: string | null;
8 release_date: string;
9 vote_average: number;
10 vote_count: number;
11 popularity: number;
12 genre_ids?: number[];
13 adult: boolean;
14}
15
16export interface TVShow {
17 id: number;
18 name: string;
19 overview: string;
20 poster_path: string | null;
21 backdrop_path: string | null;
22 first_air_date: string;
23 vote_average: number;
24 vote_count: number;
25 popularity: number;
26 genre_ids?: number[];
27 origin_country: string[];
28}
29
30export interface TMDBResponse<T> {
31 page: number;
32 results: T[];
33 total_pages: number;
34 total_results: number;
35}
36
37// Detail types
38export interface MovieDetails extends Movie {
39 budget: number;
40 revenue: number;
41 runtime: number;
42 status: string;
43 tagline: string;
44 genres: Genre[];
45 production_companies: ProductionCompany[];
46 credits?: Credits;
47 videos?: { results: Video[] };
48 images?: Images;
49}
50
51export interface TVDetails extends TVShow {
52 number_of_episodes: number;
53 number_of_seasons: number;
54 episode_run_time: number[];
55 seasons: Season[];
56 networks: Network[];
57 status: string;
58 credits?: Credits;
59 videos?: { results: Video[] };
60}
61
62export interface Genre {
63 id: number;
64 name: string;
65}
66
67export interface Video {
68 id: string;
69 key: string; // YouTube/Vimeo video ID
70 name: string;
71 site: 'YouTube' | 'Vimeo';
72 size: number;
73 type: 'Trailer' | 'Teaser' | 'Clip' | 'Featurette' | 'Behind the Scenes';
74 official: boolean;
75 published_at: string;
76}
77
78export interface Credits {
79 cast: CastMember[];
80 crew: CrewMember[];
81}
82
83export interface CastMember {
84 id: number;
85 name: string;
86 character: string;
87 profile_path: string | null;
88 order: number;
89}
90
91export interface CrewMember {
92 id: number;
93 name: string;
94 job: string;
95 department: string;
96 profile_path: string | null;
97}
98
99export interface Season {
100 id: number;
101 season_number: number;
102 name: string;
103 overview: string;
104 air_date: string;
105 episode_count: number;
106 poster_path: string | null;
107}
108
109export interface Episode {
110 id: number;
111 name: string;
112 overview: string;
113 episode_number: number;
114 season_number: number;
115 still_path: string | null;
116 air_date: string;
117 runtime: number;
118 vote_average: number;
119}
Axios Client Setup
typescript
1import axios from 'axios';
2
3const TMDB_BASE_URL = 'https://api.themoviedb.org/3';
4
5const tmdbClient = axios.create({
6 baseURL: TMDB_BASE_URL,
7 timeout: 10000,
8 headers: {
9 'Accept': 'application/json',
10 'Authorization': `Bearer ${process.env.TMDB_ACCESS_TOKEN}`,
11 },
12});
13
14// Add default language
15tmdbClient.interceptors.request.use((config) => {
16 config.params = {
17 ...config.params,
18 language: 'en-US',
19 };
20 return config;
21});
22
23// Error handling
24tmdbClient.interceptors.response.use(
25 (response) => response,
26 (error) => {
27 if (error.response?.status === 429) {
28 // Rate limited - implement retry with backoff
29 console.warn('TMDB rate limit hit');
30 }
31 return Promise.reject(error);
32 }
33);
34
35export default tmdbClient;
React Native Hooks
useTrending Hook
typescript
1import { useState, useEffect } from 'react';
2import tmdbClient from '../services/tmdbClient';
3import { Movie, TVShow, TMDBResponse } from '../types/tmdb';
4
5type MediaType = 'movie' | 'tv' | 'all';
6type TimeWindow = 'day' | 'week';
7
8export function useTrending<T extends Movie | TVShow>(
9 mediaType: MediaType = 'movie',
10 timeWindow: TimeWindow = 'week'
11) {
12 const [data, setData] = useState<T[]>([]);
13 const [loading, setLoading] = useState(true);
14 const [error, setError] = useState<Error | null>(null);
15
16 useEffect(() => {
17 let cancelled = false;
18
19 async function fetchTrending() {
20 try {
21 setLoading(true);
22 const response = await tmdbClient.get<TMDBResponse<T>>(
23 `/trending/${mediaType}/${timeWindow}`
24 );
25 if (!cancelled) {
26 setData(response.data.results);
27 }
28 } catch (err) {
29 if (!cancelled) {
30 setError(err as Error);
31 }
32 } finally {
33 if (!cancelled) {
34 setLoading(false);
35 }
36 }
37 }
38
39 fetchTrending();
40 return () => { cancelled = true; };
41 }, [mediaType, timeWindow]);
42
43 return { data, loading, error };
44}
useMovieDetails Hook
typescript
1export function useMovieDetails(movieId: number) {
2 const [movie, setMovie] = useState<MovieDetails | null>(null);
3 const [loading, setLoading] = useState(true);
4 const [error, setError] = useState<Error | null>(null);
5
6 useEffect(() => {
7 let cancelled = false;
8
9 async function fetchDetails() {
10 try {
11 setLoading(true);
12 const response = await tmdbClient.get<MovieDetails>(
13 `/movie/${movieId}`,
14 {
15 params: {
16 append_to_response: 'videos,credits,images',
17 },
18 }
19 );
20 if (!cancelled) {
21 setMovie(response.data);
22 }
23 } catch (err) {
24 if (!cancelled) {
25 setError(err as Error);
26 }
27 } finally {
28 if (!cancelled) {
29 setLoading(false);
30 }
31 }
32 }
33
34 if (movieId) {
35 fetchDetails();
36 }
37 return () => { cancelled = true; };
38 }, [movieId]);
39
40 return { movie, loading, error };
41}
useSearch Hook with Debounce
typescript
1import { useState, useCallback, useRef } from 'react';
2import { debounce } from 'lodash';
3
4export function useSearch() {
5 const [results, setResults] = useState<(Movie | TVShow)[]>([]);
6 const [loading, setLoading] = useState(false);
7 const [error, setError] = useState<Error | null>(null);
8
9 const searchRef = useRef(
10 debounce(async (query: string) => {
11 if (!query.trim()) {
12 setResults([]);
13 return;
14 }
15
16 try {
17 setLoading(true);
18 const response = await tmdbClient.get('/search/multi', {
19 params: { query },
20 });
21 setResults(response.data.results.filter(
22 (item: any) => item.media_type === 'movie' || item.media_type === 'tv'
23 ));
24 } catch (err) {
25 setError(err as Error);
26 } finally {
27 setLoading(false);
28 }
29 }, 300)
30 );
31
32 const search = useCallback((query: string) => {
33 searchRef.current(query);
34 }, []);
35
36 return { results, loading, error, search };
37}
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
typescript
1function getVideoUrl(video: Video): string {
2 if (video.site === 'YouTube') {
3 return `https://www.youtube.com/watch?v=${video.key}`;
4 }
5 if (video.site === 'Vimeo') {
6 return `https://vimeo.com/${video.key}`;
7 }
8 return '';
9}
10
11// Get official trailer
12function getOfficialTrailer(videos: Video[]): Video | undefined {
13 return videos.find(v => v.type === 'Trailer' && v.official)
14 || videos.find(v => v.type === 'Trailer')
15 || videos[0];
16}
Resources