Project Setup

The emphasis of this tutorial is on how to use GraphQL to execute all four CRUD actions in the context of a MERN stack. Part 1 covers the API and can be done without doing Part 2.

Most readers will likely be at least somewhat familiar with the MERN stack, but you should be able to build the API even if you are new to it and to GraphQL. I only assume, since you are here, that you know what GraphQL is. You should, however, be comfortable using the command line, and you need to have Node.js installed. Let's get started.

Express-graphql vs. Apollo-server: We will be using the express-graphql package to create the GraphQL HTTP server and connect it to a Node application using the Express web framework. Express-graphql was originally created by Facebook and was spun off with the rest of GraphQL to the GraphQL Foundation. Another option is to use the apollo-server package as the GraphQL HTTP server. It is part of the suite of software products built by Apollo for GraphQL. For this small app either works fine and has similar code. Apollo-server has some added features such as server side caching. To see this same tutorial using Apollo-server go to Build a GraphQL API with Node.js and Apollo-Server Tutorial.

A note on syntax. Node recognizes most, but not all, ES6 syntax. Notably it does not recognize import/export statements, so we won't use them here. While we won't do it in this tutorial, you have the option of installing Babel which will transpile ES6 to ES5 for you at run time. Also, as default I am using the declaration syntax for functions rather than arrow functions. That's because they require explicit return statements which makes the code a little bit clearer.

Create a directory for your project and cd into it. We'll call it mern-app-with-graphql (or whatever name you prefer to use).

  • mkdir mern-app-with-graphql && cd mern-app-with-graphql
  • Create the files for our API. You can use these UNIX commands from the command line, or do it in your text editor. Mkdir creates a directory and touch creates a file.

    touch server.js
    mkdir models
    touch models/article.js
    mkdir graphql
    touch graphql/schema.js
    touch graphql/resolvers.js
    

    Package.json

    Generate a package.json file. This file contains the metadata for a Node.js project. Adding the --yes flag (-y for short) will use the defaults for the fields.

  • npm init -y
  • Install our npm packages. First the global packages. These get installed on your system and not attached to any specific project. The Nodemon package will allow you to do hot reloading of the app, which means it will restart the server every time you make a change and save it in your project. The --global or -g flag makes it global. If you already have it installed this will upgrade it to the latest version.

  • npm install -g nodemon
  • Local packages will be installed directly in your project in the node_modules folder. Install the following packages:

  • npm install express mongoose graphql express-graphql
  • Your package.json file should look something like the below. A list of the project's installed packages with version and their dendencies is saved under dependencies. If we ran npm install then npm would attempt to install or update the packages listed there.

    Make sure the "main" property's value is "server.js". Change it if it isn't.

    Also make sure you have the "start" script. Add it if you don't. We can run the commands in the scripts object from the command line with npm run script-name. I'll explain more in the next section.

    // package.json
    {
      "name": "mern-app-with-graphql",
      "version": "1.0.0",
      "description": "Web API built with Node.js, Express, MongoDB, and GraphQL",
      "main": "server.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "start": "node server.js"
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "dependencies": {
        "express": "^4.17.1",
        "express-graphql": "^0.9.0",
        "graphql": "^14.4.2",
        "mongoose": "^5.6.6"
      }
    }
    

    You will also see a file named package-lock.json. That lists all the project's installed packages with their versions and their dendencies.


    Express Web Framework

    Server.js

    Populate the server.js file with a hello world app powered by Express:

    // server.js
    const express = require('express');
    const app = express();
    const PORT = 3001;
    
    app.get('/', function(request, response) { response.send('Hello World!') });
    
    app.listen(PORT, function() {console.log(`Server listening on port ${PORT}`) });
    

    This simple file, with the Express library, will create a server.

    Start the Server

    Now that you have a server for your app, there are a few different commands you can use to run it. I'll cover them briefly. First there is a standard node command to execute any file:

  • node server.js or just node server
  • This should log "Server listening on port 3001" in the terminal, and if you open a browser to localhost:3001 you should get "hello world". To stop the server, from the terminal press CTRL+C,

    NPM init generated a script to run this command in our package.json file. You call the scripts in your package.json file with npm run script-name or in a few cases "run" is optional.

  • npm start
  • The third way to execute the server.js file is to use the nodemon command. We installed nodemon for hot reloading. To use it just enter nodemon and it looks for a file named server.js to run by default. This is the command we'll use throughout this tutorial.

  • nodemon

  • MongoDB database

    MongoDB is a noSQL database. If you've only dealt with relational databases like PostgreSQL, MySQL, SQLite, etc., it's a different way of thinking about it. This tutorial won't get into how noSQL works. There are plenty of other resources for that such as mongodb.com/nosql-explained. Don't worry if you've never used it before, you can continue with this tutorial without a problem.

    MongoDB offers a cloud database called Atlas, or you can download the "community version" on your local machine. You may have read about another popular cloud solution called mlabs, but they were bought by MongoDB and are no longer accepting new accounts. Which to use? I generally prefer a local version to work with in development. Sometimes there are connection issues with a cloud version (which I ran into). But if you deploy your app to Heroku then you need a cloud solution. I will explain both ways.

    Quick terminology for those used to relational databases. "Collections" are like tables in a relational database and "documents" are like records.

    Also, don't worry about creating the collections. Once you create the database and connect to it, MongoDB will automatically create a collection for you when you save a document to a collection that doesn't exist yet.

    Option 1) MongoDB Atlas

    At the time of this writing you can set up a free account for a small project like this one at mongodb.com/cloud/atlas. It is very fast and straight-forward to set up. Just follow their steps, but note a few things:

    Option 2) MongoDB local "community" version

    I'm on a Mac so I'll explain briefly how to load the MongoDB community version on a Mac using Homebrew. If you are on windows, installation instructions are at docs.mongodb.com/manual/tutorial/install-mongodb-on-windows.

    To see if MongoDB is installed on your machine with Homebrew run:

  • brew list mongodb or brew search mongodb
  • If it's installed it will show the path of any MongoDB executable files (there are several). If it's not installed run:

  • brew install mongodb
  • Now set up the directory where the database is stored. MongoDB expects you to use data/db so we'll go with that:

  • sudo mkdir -p data/db
  • And set the owner of this directory to yourself.

  • sudo chown -R $(whoami) data/db
  • Sudo means Super User Do. You need that for commands that require super user permissions like messing around with you Mac's root directory. Chown means change owner. The -R flag makes this recursive meaning it changes the owner of all the files and folders in the directory. $(whoami) runs the UNIX whoami command to get your username on your computer.

    If MongoDB was already installed you can check to see if the version is outdated.

  • brew outdated mongodb
  • This will return your version compared to the latest version if they differ. If it returns nothing then you have the latest version. You can upgrade the MongoDB version with:

  • brew upgrade mongodb
  • Run MongoDB

    Once it's installed you run MongoDB from the terminal (from any directory) and have to leave that window open and running to access the database:

  • mongod
  • Stop MongoDB from the same terminal window with CTRL+C.

    MongoDB Shell

    You can interact with the database directly using the MongoDB shell from the Terminal. There are also GUI tools like Robo T3 if you prefer that but frankly for small projects I find the command line easier to work with. To use the shell from anywhere in the terminal run:

  • mongo
  • The list of MongoDB commands are at docs.mongodb.com/manual/reference/mongo-shell and CRUD Operations. Useful commands include:

  • show dbs - Returns a list of your databases.
  • use my_local_db - Use the specified database. Creates it if it doesn't exist.
  • db - Returns the db you are currently in.
  • show collections - Returns the collections in the db you are currently in. Collections are like tables in an SQL database.
  • db.createCollection("articles") - Create a collection.
  • db.articles.find() - Show all documents in the collection. Documents are like records in an SQL database.
  • db.articles.insertOne( { title: "Learn MongoDB", content: "Lorem Ipsum." } ) - Insert a document into a collection. Must use double quotes for a string.
  • db.articles.find({title: "Learn MongoDB"}) - Returns all that match the condition.
  • db.articles.findOne({title: "Learn MongoDB"}) - Returns the first document that matches the condition.
  • db.articles.findOne({"_id" : ObjectId("id-string-here")}); - Find by id.
  • db.articles.updateOne({title:"Learn MongoDB"},{ $set: {content:"Blah Blah."}}) - Updates a specific field in a document. Adds document if it doesn't exist. Use updateMany() for multiple.
  • db.articles.update({"_id" : ObjectId("id-string-here")},{ $set: {content:"Blah Blah Blah!"}}) - Find by ID then update.
  • db.articles.deleteOne( { title: "Learn MongoDB" }) - Remove one document by non-id field. Use deleteMany() for multiple.
  • db.articles.deleteOne( { "_id": ObjectId("value") }) - Remove by id.
  • db.dropDatabase() - Deletes the db you are currently in.

  • Mongoose ORM

    We installed the mongoose package in our app. Mongoose is a middleware library that performs the Object Relational Mapping (ORM) between our Express application and MongoDB. In essence it does the translations allowing them to talk to each other.

    We'll start by using Mongoose to connect to MongoDB in our server file. Let's replace the hello world response with our real code and then we'll go through it line by line. We'll include all the code we need for this file, but will skip over the cors and graphQL explanations for now.

    // server.js
    
    const express = require('express');                            #1
    const mongoose = require('mongoose');
    // const cors = require('cors');
    const graphqlHTTP = require('express-graphql');
    const schema = require('./graphql/schema');
    const resolvers = require('./graphql/resolvers');
    
    const app = express();                                         #2
    const PORT = 3001;                                             #3
    const MONGODB_URI = "mongodb://localhost:27017/my_local_db";   #4
    
    // app.use(cors());
    
    mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useFindAndModify: false }); #5
    mongoose.connection.once('open', function() {                  #6
      console.log('Connected to the Database.');
    });
    mongoose.connection.on('error', function(error) {
      console.log('Mongoose Connection Error : ' + error);
    });
    
    app.use("/graphql", graphqlHTTP({
      schema: schema,
      rootValue: resolvers,
      graphiql: true
    }));
    
    app.listen(PORT, function() {                                 #7
      console.log(`Server listening on port ${PORT}.`);
    });
    
    1. Start by importing our express, mongoose and cors packages, and our Express router which we'll define shortly.
    2. Calling the express() function will create our running app object.
    3. PORT: The Express listen method called at the bottom of our file uses 3000 as it's default port for it's server. We'll declare a new PORT constant. Assigning constants for things like port number and the database URL gives us the flexibility to change the values in one place depending on whether we are in development or production environments.
    4. MONGODB_URI:
      If you are using the MongoDB Atlas cloud database then paste the link there.
      If you are using the local version, MongoDB is accessed through localhost on port 27017 by default, and the path is the database name. We'll just call ours my_local_db.
    5. mongoose.connect() connects to your MongoDB database. The useNewUrlParser and useFindAndModify options are Mongoose requirements for some deprecation transitions. You'll get a warning if you leave them out.
    6. Optionally, log a message if the above connection was successful and one if it is not.
    7. Chain the Express listen method to our app. This will listen for connections on the specified port.

    Cors

    Let's talk briefly about Cross-Origin Resource Sharing (CORS). By default Express will block cross-origin HTTP requests for security reasons. Cross-origin means the request is from a different origin (domain, protocol, and/or port) than its own origin. Our API is served on localhost port 3001. If our API is accessed by mobile apps for example they will be on a different origin and any requests will be rejected by default. To allow access from any origin (or from origins that you specify) you can install and configure an npm package called cors. That will apply additional HTTP headers to tell the browser to allow cross-origin access.

    A standard MERN app is really an integrated single web application even if the back end and front end run independently. In our app we only want to give access to the integrated front end (our React client in this case). When we create the Client we'll make our API a proxy for that app (explained later) and therefore won't need the cors package.

    If you also want the API to be accessed by mobile apps or other websites then you need to install the cors middleware package and connect it to your app.

  • npm install cors
  • The server.js file comments out the cors package import and where it is applied to the app. To allow cross origin access uncomment those lines.

    For more on the CORS concept see developer.mozilla.org/en-US/docs/Web/HTTP/CORS

    The Model

    Populate the model file with the below:

    // models/article.js 
    
    const { Schema, model } = require('mongoose');
    
    const articleSchema = new Schema({                #1
      title: {
        type: String,
        required: true
      },
      content: {
        type: String,
        required: true
      }
    });
    
    module.exports = model('Article', articleSchema); #2
    
    1. A database schema is it's structure. The mongoose schema is a prototype that maps to a MongoDB collection and defines the shape of the documents within that collection. Here we are creating an instance of mongoose.Schema that defines two fields with type set to String and making them required. Mongoose Schema reference: mongoosejs.com/docs/guide.html.
    2. Models represent the data in an application. A mongoose model is a constructor function that creates and reads documents to and from the underlying MongoDB database. The first argument is the singular uppercase name of your database collection. So Article represents the articles MongoDB collection. The second argument is the schema which we defined above. An individual article is an instance of the Article model.

    GraphQL

    If you are brand new to GraphQL I recommend going through the Getting Started tutorial on their website https://graphql.org/graphql-js. This tutorial is the next level up from where that ends. I won't repeat the basics here but will apply them to a real (albeit super basic) application with an actual database. And use all four CRUD operations.

    I skipped over explaining the GraphQL specific code in the server.js file so I'll cover it now:

    // server.js
    
    const graphqlHTTP = require('express-graphql'); #1
    const schema = require('./graphql/schema');
    const resolvers = require('./graphql/resolvers');
    ...
    app.use("/graphql", graphqlHTTP({               #2
      schema: schema,                               #3
      rootValue: resolvers,
      graphiql: true
    }));
    
    1. We imported the express-graphql package which will set up our GraphQL HTTP server.
    2. Chaining Express's use method to our app object gives us access to the packages we imported. Express is connecting the "/graphql" endpoint to our graphQL HTTP server. This is the one and only route on our API. This is the fundamental difference between GraphQL and a REST API. REST APIs have different URL endpoints for different HTTP requests. GraphQL has one endpoint for all requests. We will send queries and mutations to that endpoint to read and write data.
    3. GraphqlHTTP takes an object argument with a required schema property.

    Let's start by creating the GraphQL Schema. We already created a schema in models/article.js for interfacing with MondoDB. But since GraphQL is not tied to any particular database it needs it's own schema to work with. Populate the GraphQL Schema file with the below:

    // graphql/schema.js
    
    const{ buildSchema } = require('graphql');    #1
    
    const schema =  buildSchema(`                 #2
    type Article {                                #3
      id: ID!
      title: String!
      content: String!
    }
    input ArticleInput {                          #4
      title: String!
      content: String!
    }
    type Query {                                  #5
      article(id: ID!): Article
      articles: [Article]
    }
    type Mutation {                               #6
      createArticle(articleInput: ArticleInput): Article
      deleteArticle(id: ID!): Article
      updateArticle(id: ID!, articleInput: ArticleInput): Article!
    }
    schema {                                      #7
      query: Query
      mutation: Mutation
    }
    `)
    
    module.exports = schema;                      #8
    
    1. Import the buildSchema utility from the graphql library.
    2. BuildSchema creates a GraphQLSchema object from the GraphQL schema language, which is structured like JSON. You pass it in as one big string.
    3. GraphQL, unlike JavaScript, is strongly typed meaning you have to explicitly specify the data types. Here we are defining Article as a type. And we are specifying it's fields, and what each field's data type is. The GraphQL schema language supports the scalar types of String, Int, Float, Boolean, and ID. Adding ! at the end means the field is required.
    4. We'll use an input for our Create and Update mutations. Set required string fields for title and content.
    5. List the queries we will accept and what will be returned. So here we accept a query called article with id as it's parameter that returns the Article type defined above. We also accept a query called articles and return an array of the Article type defined above. Putting Article in brackets signifies that it will be an array of articles. "Article" and "articles" are the names of resolver functions that will make the actual queries to the the mongoDB database and return the results. We could define them in this file but we'll define them in a separate resolver file.
    6. Mutations are HTTP requests to modify data. We'll define three, which will call resolver functions to create, delete, and update an Article.
    7. Put the query and mutation objects inside a schema object.
    8. Export the schema object for it to be used as the schema for our graphqlHTTP server.

    Returning to the server.js file, the graphqlHTTP server takes an optional rootValue property. This is where we will define our resolver functions for each API endpoint.

    const graphqlHTTP = require('express-graphql');
    const schema = require('./graphql/schema');
    const resolvers = require('./graphql/resolvers');
    ...
    app.use("/graphql", graphqlHTTP({
      schema: schema,
      rootValue: resolvers,
      graphiql: true
    }));
    

    In a REST API the endpoints are separate HTTP verb/URL path combinations like GET /articles, POST /articles, DELETE /articles/:id, etc. But with a GraphQL API there is only one such combination: POST /graphql. Instead our endpoints are represented by individual resolver functions. These functions are where we interact with the database. In our case it is MongoDB with Mongoose.js as our ORM. Let's define these now:

    // graphql/resolvers.js
    
    const Article = require('../models/article');                                  #1
    
    function articles() {                                                          #2
      return Article.find({});
    }
    
    function article(args) {
      return Article.findById(args.id)
    }
    
    function createArticle(args) {
      let article = new Article(args.articleInput);
      return article.save();
    }
    
    function deleteArticle(args) {
      return Article.findByIdAndRemove(args.id);
    }
    
    function updateArticle(args) {
      return Article.findByIdAndUpdate(args.id, args.articleInput, { new: true }); #3
    }
    
    module.exports = { articles, article, createArticle, deleteArticle, updateArticle }
    
    1. Import the Article model which connects us to the database.
    2. There is one resolver function per API endpoint. Each performs a CRUD action on the database and returns the result. The find, findById, save, findByIDandRemove, and findByIdAndUpdate are all methods from the Mongoose ORM.
    3. The Model.findByIdAndUpdate(id, update, options, callback) method returns the original document by default not the updated one. To return the updated object you need to add the {new: true} option.

    Test the API with Graphiql

    Returning to the graphqlHTTP object in our server.js file, the last property we are setting is:

    app.use("/graphql", graphqlHTTP({
      schema: schema,
      rootValue: resolvers,
      graphiql: true
    }));
    

    Graphiql is a web interface where you can send queries and mutations to the API. Make sure the server is running and you can test out the API.

  • nodemon
  • Open the browser to localhost:3001/graphql. There you'll see the graphiql API interface. In the panel on the left enter the createArticle mutation:

    mutation {
      createArticle(articleInput: { title: "Learn GraphQL", content: "Blah blah."}) {
        id, title, content
      }
    }
    

    This mutation will call the createArticle endpoint we defined on the resolvers file, passing in the articleInput object with values for the title and content properties. The property names in the curly braces are what we want returned if the mutation is successful. Click the execute button on the upper left and you should see the output on the panel to the right.

    Change the title and press it again so we have another article.

    Now let's do a query to to see our articles:

    query {
      articles {
        title
        content
        id
      }
    }
    

    Press the execute button and you should see the articles you created. Copy the id of one of them and enter another query. (Note: query is the default type so you can leave off the word "query" if you like).

    query {
      article(id: "article-id-here") {
        id
        title
        content
      }
    }
    

    Update that same article:

    mutation {
      updateArticle(id: "article-id-here", articleInput: { title: "Learn MongoDB", content: "Blah blah." }) {
        id, title, content
      }
    }
    

    Click execute and you should get the updated article back.
    And finally, send a delete mutation:

    mutation {
      deleteArticle(id: "article-id-here") {
        id, title, content
      }
    }
    

    Click execute and it should return the article after deleting it. There is a history button on the top and if you click it you should see all the queries and mutations you ran. Click on the articles query then execute it. The deleted article should be gone.

    There is a Docs menu on the right of the tool that lists all your query and mutation endpoints. You can drill down on it to see the available fields.

    If you are familiar with the Postman API query tool, recent versions have a GraphQL feature that allows you to do these same queries/mutations. It has the added advantage of being able to save your queries, but the disadvantage of not having the Docs menu that shows all your endpoints.


    Welp, that wraps up the API portion of our MERN with GraphQL CRUD application tutorial. If you want to add a React front end then on to part 2 Build a React app with GraphQL Tutorial