loading...

We can REST when we're dead: GraphQL, Express, and monster movies

annabellettaylor profile image Annabelle Taylor ・7 min read

I like to shake things up in my development flow every now and then to make sure I don't get too comfortable (heaven forbid the Imposter Syndrome stay away for longer than a week at a time). Right after figuring out the difference between React state and props, I threw everything I knew about state management out the window and started working through Redux. WordPress with PHP is pretty cool, but how about Gatsby?

The Great Urge struck again today and left me thinking, "RESTful APIs are soooo last season; hello, GraphQL!"

Project setup

To get started, create a new directory and npm/git init that bad boy. Since we're only interested in the backend for now, we only need to install one dependency: GraphQL-Yoga. This nifty little package gives you everything you need to start making GraphQL queries, plus spins up an instance of the ever-so-helpful GraphQL Playground. Install this with yarn add graphql-yoga (or npm install graphql-yoga if that's more your speed).

From the root of your project folder, we'll need to make a couple of files. Run the following:

mkdir src
touch src/index.js
touch src/schema.graphql

And set them up as such:

/*INDEX.JS*/

//Import the tools to create a GraphQL server from GraphQL-Yoga
const { GraphQLServer } = require("graphql-yoga");

//Add some starting seed data
let movies = [
    {
        id: `movie0`,
        title: "The Conjuring",
        year: 2013
    },
    {
        id: `movie1`,
        title: "Nightmare on Elm Street",
        year: 1984
    },
    {
        id: `movie2`,
        title: "The Hills Have Eyes",
        year: 1977
    }
];

//This will com in handy when we want to add movies
let movieID = movies.length;

//All of the queries (asking for data) and mutations (think the Create, Update,
//and Delete of CRUD) from our schema will need to be resolved. That logic goes here.
const resolvers = {
    Query: {
        allMovies: () => movies
    }


const server = new GraphQLServer({
    typeDefs: "./src/schema.graphql",
    resolvers
});

//Spin up the server with the defined-in-file resolver functions and the 
//imported type definitions
server.start(() => console.log(`Server is running on http://localhost:4000`));
/*SCHEMA.GRAPHQL*/

//What does our model look like? Exclamation points mean "this data is required!"
type Movie{
    id: ID!,
    title: String!,
    year: Int!
}

//What sort of data are we going to ask for?
type Query{
    //Notice the exclamation point at the end; this can be read as "definitely
    //return an array so that the return value is never null. Fill that array with
    //Movie instances, if at all possible" 
    allMovies: [Movie]!
}

Read all items

I'm on a bit of a horror movie kick, hence the inspiration for my seed data. As you can see, I've already added the first query/resolver combo to display all of the horror flicks. Try running the following query in GraphQL Playground (on localhost:4000) and take a look at the results:

query{
  allMovies{
    title
    year
    id
  }
}

You should get something like this:

What's really nifty about GraphQL is that well-written queries will return exactly the data that you ask for: no more, no less. Instead of grabbing everything about each movie object (as seen above), you could simply return the titles with this slight adjustment:

query{
  allMovies{
    title
  }
}

Read one item

What if we just wanted to return information about one movie by querying its ID? It'd be reasonable to try the following:

/*SCEHMA.GRAPHQL*/
type Query{
    allMovies: [Movie]!,
    findMovie(id:ID!): Movie
}

/*INDEX.JS*/
Query: {
    allMovies: () => movies,
    findMovie: (parent, args) => movies.filter(film => film["id"] == args.id)
},

But then you query it and you get an error claiming that you "cannot return null for non-nullable field Movie.title." There definitely IS a movie with the ID "movie1," so clearly that should have a title. What the heck is going on?!

Though this looks like some sort of poltergeist bug from the great beyond, it's actually a matter of nesting objects (or, more specifically, nesting objects inside of arrays inside of objects). Run the command again with these console.log statements inside of your query resolver and consider their outputs:

//Print the entire movie array
console.log(movies) =
[ { id: 'movie1', title: 'The Conjuring', year: 2013 },
{ id: 'movie2', title: 'Nightmare on Elm Street', year: 1984 },
{ id: 'movie3', title: 'The Hills Have Eyes', year: 1977 } ]

//Print an array containing the film whose ID matches the one from the arguments
film = movies.filter(film => film["id"] == args.id)
console.log(film) =
[ { id: 'movie2', title: 'Nightmare on Elm Street', year: 1984 } ]

//Print the first element from the above array
console.log(film[0]) = 
{ id: 'movie2', title: 'Nightmare on Elm Street', year: 1984 }

Do you notice the subtle difference between the second and third result? We weren't able to return the variable film by itself because it was not of type Movie. Rather, it was an array that contained a single movie instance. To get around this, edit your query resolver so that it returns the first element in that array:

/*INDEX.JS*/
Query: {
    allMovies: () => movies,
    findMovie: (parent, args) => movies.filter(film => film["id"] == args.id)[0]
}

Restart the server and run your query again. Bazinga!

Creating an object

That's all well and good, but new horror movies are being made all the time, so we need some way to add movies to our array. This introduces a new type of operation: the aptly-named "mutation."

Consider what data needs to trade hands in order to make a new movie object. According to our model, each movie has its a title, a year, and a unique ID. Since we're not working with any external APIs yet, we'll need to include the title and year by ourselves. However, inputting an ID by hand may prove perilous; what if we forget how many movies we've added? What if we forget our capitalization conventions? To stay away from such spooky possibilities, it's best if we let the program take care of the ID by itself. This is where the movieID variable comes into play!

/*SCHEMA.GRAPHQL*/
type Mutation{
    //Take in a title of type String and a year of type Int, then return a Movie
    addMovie(title:String!,year:Int!):Movie!,
}

/*INDEX.JS*/
let movieID = movies.length;
const resolvers = {
    //Our Queries will stay the same
    Query: {
        allMovies: () => movies,
        findMovie: (parent, args) => movies.filter(film => film["id"] == args.id[0]
    },
    Mutation: {
    addMovie: (parent, args) => {
            const newMovie = { 
                //Take the arguments and set them as the title and year, then
                //set the ID equal to the string "movie" plus whatever index the
                //film is getting added into
                id: `movie${movieID++}`,
                title: args.title,
                year: args.year
            };
            //Add the new object to our existing array
            movies.push(newMovie);
            //Return the new movie object to satisfy the return type set in our schema
            return newMovie;
        }
    }
}

I'm admittedly still working to figure out what's going on with that parent argument, but the mutation doesn't work without it. Language specs: can't live with 'em, can't live without 'em.

Regardless, refresh the server and try adding a movie. You should wind up with something like this:

Updating/editing a movie object

D'oh! The eagle-eyed among us will note that when I added The Shining, I accidentally set the release year to 1977. That's when the original Stephen King novel came out, but Stanley Kubrick's interpretation didn't hit the big screen until three years later in 1980. We must make amends!

Because this is editing the data as opposed to simply reading it, updating the film object will be another mutation as opposed to a root query. In terms of how to shape the mutation, consider again what information needs to go where. Querying by ID is often a good idea, especially given that we may need to update either the year or the title in any given request. However, as in this example, not every request will necessarily update both properties. We'll want to write it in a way such that up to two parameters are accepted, but not required. Finally, it's within the realm of possibility that someone could query a movie with the ID "redrum," and given how we structure our IDs that search should come up null. Therefore, we can't require that this function outputs a Movie:

/*SCHEMA.GRAPHQL*/
//inside of type Mutation
updateMovie(id:ID!,title:String,year:Int):Movie

/*INDEX.JS*/
//inside of your Mutation resolver object, underneath addMovie
updateMovie: (parent, args) => {
    const selectedMovie = movies.filter(film => film["id"] == args.id)[0];
    if (args.title) selectedMovie.title = args.title;
    if (args.year) selectedMovie.year = args.year;
    return selectedMovie;
}

The conditionals ensure that only the input data fields are updated. Back to the server and lather, rinse, repeat:

Glad we got that straightened out!

Delete a movie object

Deleting an object from our array with GraphQL combines concepts from each of the three prior functions:

  • Like READ, you query one particular movie by ID
  • Like CREATE, you're editing the sequence of the array
  • Like UPDATE, the film you're looking for could be anywhere in the array (or, in the event of a faulty input ID, could be nowhere)

Given these similarities, I'll leave this final function as an exercise (though you can visit my GitHub repo if you need a hint or want to check your work). You should end up with a final request that looks a little like this:

Going forward

Obviously, this walkthrough merely scratches the surface of everything that's possible with GraphQL. If you're interested in exploring further, I recommend checking out the following resources:

Happy querying!

Discussion

pic
Editor guide