Skip to main content

Let's build a GraphQL server with Node.js and Express (2)

Greetings!

In this third article of the GraphQL series, I would like to discuss the steps needed to create a GraphQL server using Node.js with a somewhat more advanced query than the previous one.

The complete code can be found here.

What do we build?

Movie API where we can query movies and directors' information.

Initializing the project

npm init -y
npm install express graphql express-graphql --save
npm install @babel/core @babel/node @babel/preset-env --save-dev
npm install nodemon --save-dev
Enable ES6 modules in package.json and add a start command for nodemon.
{
  "name": "nodejs-graphql-query",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "start": "nodemon --exec babel-node src/index",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.18.1",
    "express-graphql": "^0.12.0",
    "graphql": "^16.6.0"
  },
  "devDependencies": {
    "@babel/core": "^7.19.3",
    "@babel/node": "^7.19.1",
    "@babel/preset-env": "^7.19.3",
    "nodemon": "^2.0.20"
  }
}
Create .bablerc and add below to setup babel
touch .babelrc
{
  "presets": ["@babel/preset-env"]
}
Our project structure is like below;

Design the data store

To keep things simple we will use a fake database with an array. The data store has no impact on the GraphQL server. This is our movie-data-service.js module.
const movies = [
  { id: "1", title: "The Great Battle", year: 2018, directorId: "2" },
  { id: "2", title: "Baahubali: The Beginning", year: 2015, directorId: "3" },
  { id: "3", title: "Baahubali 2: The Conclusion", year: 2017, directorId: "3" },
  { id: "4", title: "Magadheera", year: 2009, directorId: "3" },
  { id: "5", title: "Rurouni Kenshin Part I: Origins", year: 2012, directorId: "4" },
  { id: "6", title: "Rurouni Kenshin Part II: Kyoto Inferno", year: 2014, directorId: "4" },
  { id: "7", title: "Rurouni Kenshin: The Legend Ends", year: 2014, directorId: "4" },
  { id: "8", title: "Avatar", year: 2009, directorId: "1" },
  { id: "9", title: "Avatar 2", year: 2023, directorId: "1" },
  { id: "10", title: "Titanic", year: 1997, directorId: "1" }
];

const directors = [
  { id: "1", name: "James Cameron" },
  { id: "2", name: "Kwang-shik Kim" },
  { id: "3", name: "S.S. Rajamouli" },
  { id: "4", name: "Keishi Otomo" }
];

export function findMovies() {
  return movies;
}

export function findMovie(id) {
  return movies.find(movie => movie.id === id);
}

export function findDirector(id) {
  return directors.find(director => director.id === id);
}

export function findMoviesByDirector(directorId) {
  return movies.filter(movie => movie.directorId === directorId);
}

Express server

Create index.js inside the root directory of the project. We are creating an express server here and separating the GraphQL setup into a separate module. We will implement graphql-schema.js later.
import express from "express";
import configureGraphql from "./graphql-schema.js";

const app = express();

configureGraphql(app);

app.listen(3000, () => console.log('Running a GraphQL API server at http://localhost:3000/graphql'));

Design the schema and object types

The first step is to design our object types. It will be a one-to-many relationship between the director and the movie. Note that the "movies" field's type is an array or MovieType.
type MovieType {
  id: String!
  title: String
  year: String
  director: DirectorType
}

type DirectorType {
  id: String!
  name: String
  movies: [MovieType]
}

GraphQL Schema

Create file graphql-schema.js inside the root directory. We will add GraphQL-related code in this module.
First of all, we need to import graphql from graphql module and graphqlHTTP from express-graphql. Make sure to import methods from the fake database service as well because we are going to access it within this module.
import { graphqlHTTP } from "express-graphql";
import graphql from "graphql";
import { findMovie, findMovies, findDirector, findMoviesByDirector } from "./movie-data-service.js";
Instead of defining the schema using GraphQL schema language, we are constructing the schema programmatically. We can do that using the GraphQLSchema constructor. In this approach, we create our Query using GraphQLObjectType. In the programmatic approach, we define a resolver in our Query type.
If you look at our database structure, the movie contains directorId. When we fetch a movie, the movie structure has the directorId, not the Director resource. Hence, in the director field, we need to define a resolver for the director.
resolve: (parent, args) => {}
The parent is the enclosing parent object which is the movie for the director. We obtain directorId from the parent/movie and fetch it from the data source. Note the director field's type is DirectorType.
const MovieType = new graphql.GraphQLObjectType({
  name: 'MovieType',
  fields: () => ({
    id: { type: graphql.GraphQLString },
    title: { type: graphql.GraphQLString },
    year: { type: graphql.GraphQLInt },
    director: {
      type: DirectorType,
      resolve: (parent, args) => {
        return findDirector(parent.directorId);
      }
    }
  })
});
Now, let's look at the DirectorType. As the director directs multiple movies, it is a collection. Hence we need to use GraphQLList as movies field type. This movies field parent is director. We fetch movies for the director by using its id.
const DirectorType = new graphql.GraphQLObjectType({
  name: 'DirectorType',
  fields: () => ({
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
    movies: {
      type: new graphql.GraphQLList(MovieType),
      resolve: (parent, args) => {
        return findMoviesByDirector(parent.id);
      }
    }
  })
});

Root Query

I would like to let my API consumers query;
  • Query movie by id
  • Query director by id
  • Query all movies
Hence our fields are movie, director, and movies, movie and director fields accept id as an argument to fetch relevant data.
const RootQueryType = new graphql.GraphQLObjectType({
  name: 'RootQueryType',
  fields: {
    movie: {
      type: MovieType,
      args: { id: { type: graphql.GraphQLString }},
      resolve: (parent, args) => {
        return findMovie(args.id);
      }
    },
    director: {
      type: DirectorType,
      args: { id: { type: graphql.GraphQLString }},
      resolve: (parent, args) => {
        return findDirector(args.id);
      }
    },
    movies: {
      type: new graphql.GraphQLList(MovieType),
      resolve: () => {
        return findMovies();
      }
    }
  }
});
The next step is to create the schema using GraphQLSchema providing the root query.
const schema = new graphql.GraphQLSchema({ query: RootQueryType });
Finally, we configure the endpoint that exposes GraphQL schema.
function configureGraphql(app) {
  app.use('/graphql', graphqlHTTP({
    schema: schema,
    graphiql: true,
  }));
}
Note that we have exposed graphql UI by giving graphiql: true.
We export the function so that we can pass the express app that we created in index.js.
export default configureGraphql;

Demo

npm start
We can access our application in the browser at http://localhost:3000/graphiql.



Next steps

Did you notice that when you fetch all movies, there are multiple calls to fetch director information? This is the (N+1) problem in GraphQL. Add a console log into findDirector function and try it. We are going to solve it in our next article.

Comments