Our goal is to learn how to interact with The Movie Database (TMDB) API. TMDB's API is used by thousands of applications today and provides programmatic access to a vast database of movies and TV shows. Through this lesson, we'll learn how to fetch movie information and rate them.
The first step of working with any API is collecting a few pieces of information:
In order to answer these questions, we'll start by browsing the official TMDB API documentation. The documentation provides comprehensive information about each endpoint, including the location of resources and requirements for interacting with them.
Scrolling to the left column we'll find Movies section with "Details" page. Clicking this will display documentation for it:
Putting everything together, we now know the following about the request we need to make:
https://api.themoviedb.org/3/movie/{movie_id}
27205
, which corresponds to the movie "Inception".
TMDB requires all API access be done over HTTPS to improve the security of its system. This is why the scheme begins with https and not http.
Let's try to get details for "Inception" (movie ID: 27205) without specifying an API key:
$ http GET https://api.themoviedb.org/3/movie/27205
HTTP/1.1 401 Unauthorized
Alt-Svc: h3=":443"; ma=86400
Cache-Control: public, max-age=300
Connection: keep-alive
Content-Type: application/json; charset=utf-8
Date: Mon, 25 Nov 2024 18:11:11 GMT
Server: openresty
Transfer-Encoding: chunked
Vary: Origin
Via: 1.1 40bdf73541ecf41ea1aa6f68489f3e2e.cloudfront.net (CloudFront)
X-Amz-Cf-Id: xyv_3tLoYORixEC9CTGNdftsF3ni7HwYdFuuuRouroJGaLHR1xzC7Q==
X-Amz-Cf-Pop: BUD50-P2
X-Cache: Error from cloudfront
{
"status_code": 7,
"status_message": "Invalid API key: You must be granted a valid key.",
"success": false
}
As expected, we get an error telling us we need proper authentication to access any data. TMDB requires an Access Token for authentication.
Now that we've established we need authentication, let's set up our credentials. This requires a few steps:
For the purpose of this exercise you can simply enter http://localhost:3000
as the URL as we won't be using it.
Before we can make any requests, we need to set up authentication:
The Read Access Token allows us to make authenticated requests without implementing a full OAuth flow. This makes it perfect for learning API operations.
Now that we have our API access token, let's try to get information about "Inception" again:
http GET https://api.themoviedb.org/3/movie/27205 \
"Authorization: Bearer YOUR_ACCESS_TOKEN"
HTTP/1.1 200 OK
Alt-Svc: h3=":443"; ma=86400
Cache-Control: public, max-age=2884
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:33:26 GMT
ETag: W/"9dfd35b0a91ac947a4e2c12a7c040c42"
Server: openresty
Transfer-Encoding: chunked
Vary: Accept-Encoding,accept-encoding, Origin
Via: 1.1 92f3d3fb9fe3ca5166aa49ba6fcab7b6.cloudfront.net (CloudFront)
X-Amz-Cf-Id: AIhCk-S3hNHlRnH0AzQdmf1_8GazFPCRHaCJ4_7bYLmvAatQW09Pag==
X-Amz-Cf-Pop: MXP53-P4
X-Cache: Miss from cloudfront
x-memc: HIT
x-memc-age: 24625
x-memc-expires: 2884
x-memc-key: ba0bb4b9033cab23506074980327c2f8
{
"adult": false,
"backdrop_path": "/8ZTVqvKDQ8emSGUEMjsS4yHAwrp.jpg",
"belongs_to_collection": null,
"budget": 160000000,
"genres": [
{
"id": 28,
"name": "Action"
},
{
"id": 878,
"name": "Science Fiction"
},
{
"id": 12,
"name": "Adventure"
}
],
"homepage": "https://www.warnerbros.com/movies/inception",
"id": 27205,
"imdb_id": "tt1375666",
"origin_country": [
"US",
"GB"
],
"original_language": "en",
"original_title": "Inception",
"overview": "Cobb, a skilled thief who commits corporate espionage by infiltrating the subconscious of his targets is offered a chance to regain his old life as payment for a task considered to be impossible: \"inception\", the implantation of another person's idea into a target's subconscious.",
"popularity": 116.011,
"poster_path": "/ljsZTbVsrQSqZgWeep2B1QiDKuh.jpg",
"production_companies": [
{
"id": 923,
"logo_path": "/8M99Dkt23MjQMTTWukq4m5XsEuo.png",
"name": "Legendary Pictures",
"origin_country": "US"
},
{
"id": 9996,
"logo_path": "/3tvBqYsBhxWeHlu62SIJ1el93O7.png",
"name": "Syncopy",
"origin_country": "GB"
},
{
"id": 174,
"logo_path": "/zhD3hhtKB5qyv7ZeL4uLpNxgMVU.png",
"name": "Warner Bros. Pictures",
"origin_country": "US"
}
],
"production_countries": [
{
"iso_3166_1": "GB",
"name": "United Kingdom"
},
{
"iso_3166_1": "US",
"name": "United States of America"
}
],
"release_date": "2010-07-15",
"revenue": 825532764,
"runtime": 148,
"spoken_languages": [
{
"english_name": "English",
"iso_639_1": "en",
"name": "English"
},
{
"english_name": "French",
"iso_639_1": "fr",
"name": "Français"
},
{
"english_name": "Japanese",
"iso_639_1": "ja",
"name": "日本語"
},
{
"english_name": "Swahili",
"iso_639_1": "sw",
"name": "Kiswahili"
}
],
"status": "Released",
"tagline": "Your mind is the scene of the crime.",
"title": "Inception",
"video": false,
"vote_average": 8.369,
"vote_count": 36565
}
The response JSON contains much more information than what you might typically see on TMDB's website. While some fields like title
and release_date
are self-explanatory, others require some explanation. For instance, backdrop_path
is a partial URL that needs to be combined with TMDB's base image URL to get the full movie backdrop image. The budget
and revenue
fields are in US dollars, and popularity
is a floating-point number that indicates the current trending status of the movie.
Note that the genres
field contains an array of objects, each with its own id
and name
. This is a common pattern in APIs where related data (in this case, movie genres) is embedded within the main resource. TMDB has detailed documentation about these genre IDs and their meanings.
Let's see what happens when we request a movie that doesn't exist:
$ http GET https://api.themoviedb.org/3/movie/999999999 \
"Authorization: Bearer YOUR_ACCESS_TOKEN"
HTTP/1.1 404 Not Found
Alt-Svc: h3=":443"; ma=86400
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:34:31 GMT
Server: openresty
Transfer-Encoding: chunked
Vary: Accept-Encoding,accept-encoding, Origin
Via: 1.1 5d05f2fc24319f66178c71a89f65afb4.cloudfront.net (CloudFront)
X-Amz-Cf-Id: rLB6M2TWJqmf4oQTHHXGEeONIqrtOC6-mxJXGhFQbnGLE08ngQqIAg==
X-Amz-Cf-Pop: MXP53-P4
X-Cache: Error from cloudfront
{
"status_code": 34,
"status_message": "The resource you requested could not be found.",
"success": false
}
A 404 Not Found
response makes sense as this movie ID doesn't exist in their database.
Now that we can fetch movie information, let's try rating a movie. According to the documentation, we need:
https://api.themoviedb.org/3/movie/{movie_id}/rating
Let's try to rate "Inception":
$ http POST https://api.themoviedb.org/3/movie/27205/rating \
"Authorization: Bearer YOUR_ACCESS_TOKEN" \
value:=9
HTTP/1.1 201 Created
Alt-Svc: h3=":443"; ma=86400
Connection: keep-alive
Content-Length: 94
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:35:08 GMT
Server: openresty
Via: 1.1 f0d757c9b2cb754e4844ae59614d9100.cloudfront.net (CloudFront)
X-Amz-Cf-Id: smAtKtrmER6WJRzAhWarhRMuG5hLTrAgNbRZWgKjuoDoWwjnKKfKnA==
X-Amz-Cf-Pop: SOF50-P2
X-Cache: Miss from cloudfront
cache-control: public, max-age=0
etag: W/"04302547257ccb7e68844cdd59055002"
vary: accept-encoding, Origin
{
"status_code": 1,
"status_message": "Success.",
"success": true
}
The response confirms our rating was recorded successfully. It's worth noting that while the request seems simple, TMDB is doing several things behind the scenes:
If we wanted to submit an updated rating for the same movie we would send a new POST
request with updated value:
$ http POST https://api.themoviedb.org/3/movie/27205/rating \
"Authorization: Bearer YOUR_ACCESS_TOKEN" \
value:=10
HTTP/1.1 201 Created
Alt-Svc: h3=":443"; ma=86400
Connection: keep-alive
Content-Length: 94
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:35:53 GMT
Server: openresty
Via: 1.1 3189d5a79867614f76bbd8fdfe668c5c.cloudfront.net (CloudFront)
X-Amz-Cf-Id: z3qcDcgdZHsgZgvxN5X_hSfaNikIE6Jzxiqzr18-pfHwh_Tn8v_zKQ==
X-Amz-Cf-Pop: SOF50-P2
X-Cache: Miss from cloudfront
cache-control: public, max-age=0
etag: W/"04302547257ccb7e68844cdd59055002"
vary: accept-encoding, Origin
{
"status_code": 1,
"status_message": "Success.",
"success": true
}
If we try to submit an invalid rating:
$ http POST https://api.themoviedb.org/3/movie/27205/rating \
"Authorization: Bearer YOUR_ACCESS_TOKEN" \
value:=11.0
HTTP/1.1 400 Bad Request
Alt-Svc: h3=":443"; ma=86400
Connection: keep-alive
Content-Length: 112
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:36:56 GMT
Server: openresty
Via: 1.1 ae0b3d83f44efc7ed7498f0d5e1a0dd2.cloudfront.net (CloudFront)
X-Amz-Cf-Id: jpQKZV0fT9o2Zp9eZS8knckLloPo1EjRkAv3IFuasosg2ikVkV0MwQ==
X-Amz-Cf-Pop: SOF50-P2
X-Cache: Error from cloudfront
cache-control: public, max-age=0
vary: accept-encoding, Origin
{
"status_code": 18,
"status_message": "Value too high: Value must be less than, or equal to 10.0.",
"success": false
}
To delete our rating, we use the DELETE HTTP method on the same endpoint:
$ http DELETE https://api.themoviedb.org/3/movie/27205/rating \
"Authorization: Bearer YOUR_ACCESS_TOKEN"
HTTP/1.1 200 OK
Alt-Svc: h3=":443"; ma=86400
Connection: keep-alive
Content-Encoding: gzip
Content-Type: application/json;charset=utf-8
Date: Mon, 25 Nov 2024 18:37:21 GMT
Server: openresty
Transfer-Encoding: chunked
Vary: Accept-Encoding, accept-encoding, Origin
Via: 1.1 956fe4e84d87237dd08155132ba1c0a2.cloudfront.net (CloudFront)
X-Amz-Cf-Id: 2DMkU1CC0fgHU0XpAPwxteUOgS_BmsWEJxOZWikRP5Wcyn6n8YAyKA==
X-Amz-Cf-Pop: SOF50-P2
X-Cache: Miss from cloudfront
cache-control: public, max-age=0
etag: W/"0566f98871ddfd7f6ade28aaefb5167d"
{
"status_code": 13,
"status_message": "The item/record was deleted successfully.",
"success": true
}
Let's look at how TMDB's API design holds up against REST principles and common API practices.
The TMDB API does resource design well. They use straightforward paths like /movie/{id}
and /movie/{id}/rating
that clearly show what you're working with and how different resources relate to each other. Instead of using URLs with verbs (like/getMovie
or /createRating
), they stick to identifying resources - which is exactly what REST suggests.
Here's a comparison of some common operations:
Objective | HTTP Method | Path |
---|---|---|
Get movie details | GET | /movie/{id} |
Rate a movie | POST | /movie/{id}/rating |
Delete a rating | DELETE | /movie/{id}/rating |
This makes sense - you use GET method to retrieve data, POST to create something new, and DELETE to remove things. It's consistent and easy to understand.
One issue is how they handle updates. When you want to change a movie rating, you use POST
- the same method you'd use to create a new rating. In REST, we'd typically use PUT
or PATCH
for updates.
Their approach does make things simpler for developers since you don't need to check if a rating exists before deciding whether to use POST
or PUT
. But it's not exactly RESTful. This kind of trade-off is pretty common in real APIs, where making things easier to use sometimes wins over following REST principles perfectly.
The way TMDB handles status codes is unconventional but serves a specific purpose. Look at what they return:
{
"status_code": 7,
"status_message": "Invalid API key: You must be granted a valid key.",
"success": false
}
TMDB uses custom numeric codes to provide more detailed error information within HTTP status code categories. For example, different types of authentication failures might all return HTTP 401, but TMDB's custom codes (like 7) let developers handle specific cases with more granularity. You can find the list of errors here.
However, some aspects could still be improved:
A clearer approach, in our opinion, while maintaining the same granularity would be:
HTTP/1.1 401 Unauthorized
{
"code": "INVALID_API_KEY",
"message": "Invalid API key: You must be granted a valid key",
}
This would give you the same granular error handling but with more immediately understandable error types.
We can't know for certain why TMDB made this design decision, but using POST
instead of PUT
was likely due to the simpler usage for clients, as they don't need to first check if a rating exists before deciding whether to use POST
or PUT
. Another question you might have is, if this is not following the best practices, why don't they just update their API?
Modifying public APIs has to be done very thoughtfully, and usually removing resources or parameters will require releasing a new version of the API. This allows consumers to opt-in to the API changes and goes a long way to preventing breaking changes from disrupting existing API clients.
In addition, while these adjustments might make the API a bit more conventional, it would otherwise not change the core functionality of the API. It's hard to make a strong case for potentially disruptive changes that will involve a lot of work for engineers (both inside and outside of TMDB) when there isn't a clear functional benefit resulting from the efforts.
The TMDB API shows us that even widely-used APIs aren't perfect. Some parts, like their resource design, are done well. Others, like their status codes, could be better. This gives us several important lessons:
The most important takeaway is that successful integration with any API comes down to understanding its specific behavior through documentation and testing, rather than relying on assumptions or theoretical principles.
Use the TMDB search API to find the IDs for three movies of your choice.
Look up the watchlist endpoints in the TMDB documentation. Add the three movies from Exercise 1 to your watchlist.