Creating a movie website with GraphQL and React - part one
aurel kurtula
Posted on April 3, 2018
As I hope I explained on my previous tutorial GraphQL is another way of creating an API. Using GraphQL instead of a JSON or XML API you are giving the consumers of your API the ability to query the data they require instead of giving them everything regardless of their requirements.
In the following set of tutorials I am going to go through how we can create a GraphQL API by gathering data from existing JSON APIs and then how we can consume the newly GraphQL API using react.
About the JSON API
We'll use themoviedb "is available for everyone to use". It gives us access to their movie collection. So instead of creating our own movie database we'll access theirs through their JSON API. All you need is to create an account with them so that you get your API key - something you'll need in order to work through this series of tutorials.
In order to create our application, these are all the requests we'll need to make (https://api.themoviedb.org/3/movie/
being the root for all of them):
/${id}/videos?api_key=${YOUR_API}&language=en-US
/now_playing?api_key=${YOUR_API}&language=en-US&page=1
/${id}?api_key=${YOUR_API}&language=en-US&page=1
/${id}/reviews?api_key=${YOUR_API}&language=en-US&page=1
/${id}/credits?api_key=${YOUR_API}&language=en-US&page=1
As you can imagine we can ignore GraphQL and go straight into creating the react app and have it make the five get requests when needed. However, we are going to use GraphQL to handle the above. Hence the react developers (even though we'll do both) can make one request to the GraphQL server and pick whatever is needed.
Again, if you want to follow along go to themoviedb and create the API key you'll need soon. Further, you're welcome to use my minimal-node-environment setup, it doesn't have any code related to what we need to learn here, it just sets the environment. You can read how that environment is created here.
Step One: Linking GraphQL to Express
The following is covered in the first graphql tutorial
We need to get express to use GraphQL at a specific path. The full code at ./server.js
should look like this
import express from 'express';
import dotenv from 'dotenv';
//...
import expressGraphQL from 'express-graphql';
import schema from './schema/schema'; // our schema file
dotenv.config()
const app = express();
app.use(webpackMiddleware(webpack(webpackConfig)));
app.use('/graphql', expressGraphQL({
schema,
graphiql: true
}))
app.listen(4000, () => {
console.log('Listening');
});
From this point on, everything we'll code is going to be contained within ./schema/schema.js
so create that file.
Finally for this first step create a .env
file in the root and add your movie API like so
API=api-string-without-quotes
All that's left is to install the packages we required above
npm i -S express express-graphql graphql dotenv
Step Two: Create API endpoint for new movies
Inside ./schema/schema.js
we are able to get data from other sources, be they databases or external APIs and use them to build our own GraphQL API. Here we'll get data from five different JSON API endpoints and architect out GraphQL in a way where other would consume it as if the data comes from one call, or rather without caring where the original data comes from.
We'll start by pulling data from themoviedb concerning the movies that are currently in the cinema
https://api.themoviedb.org/3/movie/now_playing?api_key=${YOUR_API}&language=en-US&page=1
If you provide your own API and navigate to the above URL, you'll see each movie would be formatted like so:
{
results: [
{
vote_count: 3742,
id: 284054,
video: false,
vote_average: 7.3,
title: "Black Panther",
popularity: 246.001551,
poster_path: "/uxzzxijgPIY7slzFvMotPv8wjKA.jpg",
original_language: "en",
original_title: "Black Panther",
genre_ids: [28, 12, 14, 878],
backdrop_path: "/b6ZJZHUdMEFECvGiDpJjlfUWela.jpg",
adult: false,
overview: "King T'Challa returns ....",
release_date: "2018-02-13"
}
]
}
As the architects of our GraphQL API we decide which of that information is useful and which is not. We're also able to rename any of the key names.
Lets work on ./schema/schema.js
The above JSON result helps us get into GraphQL reasoning.
- We know that each movie is an object
- We'll need an ID and that's an integer
- We'll also take the
poster_path
andtitle
, and those are both strings - We aren't interested in anything else
Let's tell GraphQL that that's what a newly released movie should be.
import {
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLSchema, // we'll use them for the RootQuery
GraphQLList // we'll use them for the RootQuery
} from 'graphql';
const NewMoviesType = new GraphQLObjectType({
name: 'NewMovies',
fields:{
id: {type: GraphQLInt},
poster_path: {type: GraphQLString},
title: {type: GraphQLString},
}
})
As you can imagine that's just one half of the complete code. That's just saying we need an Object with an integer and two strings.
Finally we need to feed it the data inside the Root Query. As I said in the tutorial where I introduced GraphQL, a "root query is the entry point to GraphQL, it is the thing that fires first and in turn exposes other resources".
import axios from 'axios';
...
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields:{
newMovies: {
type: new GraphQLList(NewMoviesType),
resolve() {
return axios.get(`https://api.themoviedb.org/3/movie/now_playing?api_key=${process.env.API}&language=en-US&page=1`)
.then(res => {
const movies = res.data.results;
movies.map(movie=> movie.poster_path = "https://image.tmdb.org/t/p/w500"+movie.poster_path
)
return movies
})
}
}
}
})
As we'll see, inside the RootQuery
we'll have everything that the GraphQL API is going to make accessible. So far the Root Query has one field called newMovies
. That field is going to be an object, the blueprint of which we already created. In reality, it's not going to be one object, but an array of movie objects, hence GraphQLList(NewMoviesType)
Finally the resolve method is what feeds data to our object.
We are using axios to fetch the data from the external API (equally, if the data was stored in a database it would be retrieved within the resolve method).
Because the movie API we are fetching from is json, it works so well that simply returning the results would have worked fine:
resolve() {
return axios.get(`https://api.themoviedb.org/3/movie/now_playing?api_key=${process.env.API}&language=en-US&page=1`)
.then(res => res.data.results )
}
But of course we needed to modify the value of the poster_path
since the relative path would not have been of any use.
Finally we export the root query and run the project (npm start
)
export default new GraphQLSchema({
query: RootQuery
})
If you've been following along, navigating to http://localhost:4000/graphql
and adding the query below, you should have gotten all the latest movies.
{
newMovies{
id
title
poster_path
}
}
One got, four to come!
Step Three: Get information for a single movie
Think about it, the above query gives us all the new movies. Next, one might want more information for any one of those movies.
The existing API endpoint for that is
https://api.themoviedb.org/3/movie/${id}?api_key=${YOUR_API}&language=en-US
The result is one object with all the information for a specific movie. Let's create the GraphQL Object as we did before.
const MovieInfoType = new GraphQLObjectType({
name: 'MovieInfo',
fields: {
id: {type: GraphQLInit},
overview: {type: GraphQLString},
title: {type: GraphQLString},
poster_path: {type: GraphQLString},
genres: {type: GraphQLString},
release_date: {type: GraphQLString},
vote_average: {type: GraphQLString},
production_companies: {type: GraphQLString},
vote_average: {type: GraphQLString},
runtime: {type: GraphQLString}
}
})
As you can see, once you get your head around it, it's all repetitive.
We then need to add it inside the Root Query.
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields:{
topMovies: {...},
movieInfo: {
type: MovieInfoType,
args: {id: {type: GraphQLString}},
resolve(parentValue, args) {
return axios.get(`https://api.themoviedb.org/3/movie/${args.id}?api_key=${process.env.API}&language=en-US&page=1`)
.then(res => {
const movie = res.data;
movie.genres = movie.genres.map(g => g.name).join(', ')
movie.production_companies = movie.production_companies.map(c => c.name).join(', ')
movie.runtime+= ' min.'
return movie
})
}
}
}
})
This is a lot more complex then the topMovies
query but not hard to understand.
First we specify that for a query to movieInfo
an id
should be provided as a string. That's of course because the original API requires it. Note how we don't make use of parentValue
but the required id
get's added into the args
object.
In the original API, the genres
and production_companies
and represented as arrays:
genres: [
{
id: 18,
name: "Drama"
},
{
id: 10751,
name: "Family"
}
],
production_companies: [
{
id: 9195,
logo_path: "/ou5BUbtulr6tIt699q6xJiEQTR9.png",
name: "Touchstone Pictures",
origin_country: ""
}
],
The GraphQL Object which we created requires both the genres and production companies to be string (not that we couldn't specify them as arrays). Hence joining only their names.
Similarly, the movie length is given in minutes so I decided to append min.
to the original string - and argument can be made that that's not a good idea. When we create the interface we might want to convert those minutes into human readable time and so by adding the min.
we are giving react developers more work, but heck this is fun!
Step Four: Get Movie trailers
I'll tell you, this themoviedb.org API is fantastic. Amongst other things that we'll not cover here at all, it also gives us movie trailers. They can be accessed at
https://api.themoviedb.org/3/movie/${id}/videos?api_key=${YOUR_API}&language=en-US
The result being a reference to an array of youtube videos
{
id: 43535,
results: [
{
id: "533ec6a1c3a3685448004f82",
iso_639_1: "en",
iso_3166_1: "US",
key: "e7bD5BNqfwY",
name: "A Simple Twist of Fate - Hallmark Channel",
site: "YouTube",
size: 360,
type: "Trailer"
}
]
}
I have to say when I saw that I was slightly over excited! Anyway, let's create the video GraphQL object. We know that the only useful information there is the key
, but simply to make developing a react App easy, we know that the id
is handy, hence:
const VideoType = new GraphQLObjectType({
name: 'Video',
fields:{
id: {type: GraphQLString},
key: {type: GraphQLString}
}
})
Now, if you haven't seen the brightness of GraphQL already, be prepared to be blinded because this is where I think GraphQL shines even brighter!
If we follow the pattern we've used so far, movieVideo
would be another query in the root. But no, in reality, we know, or we're going to decide, that developers that will consume our GraphQL API are going to want the trailers when they request a single movie.
So we want the trailers to be accessible from within the movieInfo
query, hence the VideoType
array will be part of the MovieInfoType
object!
const MovieInfoType = new GraphQLObjectType({
name: 'MovieInfo',
fields: {
id: {type: GraphQLString},
...
videos: {
type: new GraphQLList(VideoType),
args: {id: { type: GraphQLString } },
resolve(parentValue, args) {
return axios.get(`https://api.themoviedb.org/3/movie/${parentValue.id}/videos?api_key=${process.env.API}&language=en-US`)
.then(res => res.data.results)
}
}
}
})
Note, we do nothing to the RootQuery
, we instead add a videos
field inside the MovieInfoType
and we resolve it there. The movie id
required to fetch the specific videos is retrieved from the siblings of video
hence fields.id
is referred to as parentValue.id
.
If we navigate to http://localhost:4000/graphq
and add the following query:
{
movieInfo(id:"284054"){
title
videos{
id
key
}
}
}
You get a bunch of Black Panther videos.
Now, that is cool! (both GraphQL and the movie)
Step five: Credits and reviews
Finally let's add movie reviews and movie cast into our GraphQL API.
const MovieCreditsType = new GraphQLObjectType({
name: 'MovieCredits',
fields:{
id: {type: GraphQLString},
character: {type: GraphQLString},
name: {type: GraphQLString},
profile_path: {type: GraphQLString},
order: {type: GraphQLString}
}
})
const MovieReviewsType = new GraphQLObjectType({
name: 'MovieReviews',
fields:{
id: {type: GraphQLString},
content: {type: GraphQLString},
author: {type: GraphQLString},
}
})
Similar to the video trailers, the movie credits and movie reviews are only meaningful in the context of the movie they are related to. So we'll add them inside the MovieInfoType
Object.
const MovieInfoType = new GraphQLObjectType({
name: 'MovieInfo',
fields: {
id: {type: GraphQLString},
...
videos: { ... },
movieReviews: {
type: new GraphQLList(MovieReviewsType),
args: {id: {type: GraphQLString}},
resolve(parentValue, args) {
return axios.get(`https://api.themoviedb.org/3/movie/${parentValue.id}/reviews?api_key=${process.env.API}&language=en-US&page=1`)
.then(res => res.data.results)
}
},
movieCredits: {
type: new GraphQLList(MovieCreditsType),
args: {id: {type: GraphQLString}},
resolve(parentValue, args) {
return axios.get(`https://api.themoviedb.org/3/movie/${parentValue.id}/credits?api_key=${process.env.API}&language=en-US&page=1`)
.then(res => res.data.cast.filter(cast => cast.profile_path ) )
}
}
}
})
Final Words
As you have gathered, just because we used, say the VideoType
inside the MovieInfoType
object, we can easily duplicate the code and add it as the child RootQuery
fields
const MovieInfoType = new GraphQLObjectType({
name: 'MovieInfo',
fields: {
id: {type: GraphQLString},
...
videos: { ... },
...
}
})
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields:{
videos: {
type: new GraphQLList(VideoType),
args: {id: { type: GraphQLString } },
resolve(parentValue, args) {
return axios.get(`https://api.themoviedb.org/3/movie/${args.id}/videos?api_key=${process.env.API}&language=en-US`)
.then(res => res.data.results)
}
},
newMovies: { ... } ,
movieInfo: { ... }
}
})
Hence, videos can then be queried in two ways
{
videos(id:"284054") {
id
key
}
}
Or through a movie
{
movieInfo(id:"284054"){
videos {
id
key
}
}
}
That's all there's is. That makes up our GraphQL API. The full code is on github, the branch Graphql-api
holds the code explored here, where as the master branch is subject to change based on the next tutorial - where well consume this new API in a react app.
You can play with a live demo here
Posted on April 3, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.