Part 1 of a 2 part series on building a MERN (Mongo-Express-React-Node) app with all four CRUD actions (Create-Read-Update-Delete) using GraphQL.
By Steve Carey - 7/24/2019
Part 2: Build a React app with GraphQL Tutorial
Finished code: Github
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
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.
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.
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 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.
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:
mongodb+srv://user:password@cluster-number.mongodb.net/test?retryWrites=true&w=majority
. Copy this and paste it into the server.js file (which we'll cover next).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
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.
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.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}.`); });
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.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
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
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 }));
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
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 }
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