Creating a movie website with GraphQL and React - part one

aurelkurtula

aurel kurtula

Posted on April 3, 2018

Creating a movie website with GraphQL and React - part one

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
Enter fullscreen mode Exit fullscreen mode

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');
});

Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

All that's left is to install the packages we required above

npm i -S express express-graphql graphql dotenv
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. We know that each movie is an object
  2. We'll need an ID and that's an integer
  3. We'll also take the poster_path and title, and those are both strings
  4. 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},

    }
})
Enter fullscreen mode Exit fullscreen mode

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
              })
            }
          }          
    }
})
Enter fullscreen mode Exit fullscreen mode

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 )
}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}
    }
})
Enter fullscreen mode Exit fullscreen mode

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
                })
            }
        }
    }
})
Enter fullscreen mode Exit fullscreen mode

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: ""
    }
],
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

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}

    }
})
Enter fullscreen mode Exit fullscreen mode

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)
             }
         }
    }
})
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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},
    }
})
Enter fullscreen mode Exit fullscreen mode

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 ) )
            }
          }
    }
})
Enter fullscreen mode Exit fullscreen mode

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: { ... }         
    }
})
Enter fullscreen mode Exit fullscreen mode

Hence, videos can then be queried in two ways

{
    videos(id:"284054") {
      id
    key
    }
}
Enter fullscreen mode Exit fullscreen mode

Or through a movie

{
   movieInfo(id:"284054"){
    videos {
      id
      key
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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

💖 💪 🙅 🚩
aurelkurtula
aurel kurtula

Posted on April 3, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related