Part 1 of a 2 part series on building a GraphQL MERN Stack (MongoDB-Express-React-Node) with Apollo that performs all four CRUD operations (Create-Read-Update-Delete).
By Steve Carey - 8/11/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 Node.js with MongoDB API. The second part builds a React client for our API. You can do Part 1 without doing Part 2.
You should be able to build the API even if you are new to Node.js and to GraphQL. You do, however, need to be comfortable using the command line, and have Node.js installed.
GraphQL is a query and manipulation language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL was developed internally by Facebook in 2012. It was released publicly in 2015, and in 2018 was spun off into the GraphQL Foundation, hosted by the non-profit Linux Foundation.
The standard architecture on the web for interacting with a database is called REST (Representational State Transfer). It has separate HTTP method and URL combinations (called endpoints) for each CRUD action you want to perform. For example, to get a list of articles you would make an HTTP GET request to example.com/articles. To post an article you would make an HTTP POST request to example.com/articles. To delete an article you would make a DELETE request to example.com/articles/:id, and so on.
GraphQL has only one endpoint, a POST request to your domain. In your API you set up responses to specific named queries and mutations that you expect to receive. Queries are readrequests (GET) while Mutations are write requests (POST, PUT or PATCH, and DELETE). The advantage to using GraphQL is the client can request exactly what they need in one request, which the API can then process as one request. If your app displays a lot of different data on one page the advantage of this architecture becomes obvious. For a simple app though, GraphQL would be overkill, so REST will still have its place.
Apollo is a software company that provides a number of tools and services around the GraphQL query language. On the client side Apollo has some very useful features for React that work with Angular and Vue as well.
For a GraphQL API you need to install the graphql npm package to process query and mutation requests. GraphQL is database agnostic meaning you can use whatever database you prefer. You still use the same ORM to make the database queries, in our case we will be using Mongoose.js with a MongoDB database.
To use GraphQL in a Node app you need a GraphQL HTTP Server. There are two popular npm packages that provide one: apollo-server and express-graphql.
Which is better? Well, they both get the job done. And for the simple app we are going to build either will work just as well and the code is pretty similar. Down the line Apollo Server does have some features that make it a good option. It allows you to cache data on the server side. It also has good error messaging capabilities. In this tutorial we will use Apollo-server.
The other option, Express-graphql, was created by Facebook and is now maintained by the GraphQL Foundation. In terms of npm downloads, it is roughly equal to apollo-server (including apollo-server-express). To see an alternate version of this tutorial using express-graphql go to: Build a GraphQL API with Node, Express, and MongoDB 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.
Create a directory for your project and cd into it. We'll call it graphql-node-apollo-server-and-react. I know that's a lot of typing so use a shorter name if you like.
mkdir graphql-node-apollo-server-and-react && cd graphql-node-apollo-server-and-react
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/typeDefs.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 mongoose graphql apollo-server
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 you run 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. You 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.
1 {
2 "name": "graphql-node-apollo-server-and-react",
3 "version": "1.0.0",
4 "description": "GraphQL API built with Node.js, Apollo-Server, MongoDB, and React",
5 "main": "server.js",
6 "scripts": {
7 "test": "echo \"Error: no test specified\" && exit 1",
8 "start": "node server.js"
10 },
11 "keywords": [],
12 "author": "",
13 "license": "ISC",
14 "dependencies": {
15 "apollo-server": "^2.8.1",
16 "graphql": "^14.4.2",
17 "mongoose": "^5.6.6"
18 }
22 }
You will also see a file named package-lock.json. That lists all the project's installed packages with their versions and their dendencies.
Following the ancient traditions of my people we will start things off with a Hello World app powered by Apollo-Server. Actually this is the same Hello World app from the Apollo-Server docs. We'll use it as our starting point.
1 const { ApolloServer, gql } = require('apollo-server');
2
3 const PORT = 4000;
4
5 // The GraphQL schema
6 const typeDefs = gql`
7 type Query {
8 hello: String
9 }
10 `;
11
12 // A map of functions which return data for the schema.
13 const resolvers = {
14 Query: {
15 hello: function() { return 'world' }
16 }
17 };
18
19 const server = new ApolloServer({
20 typeDefs,
21 resolvers,
22 });
23
24 server.listen(PORT).then(function({ url }) {
25 console.log(`Server listening at ${url}.`);
26 });
This simple file will create a GraphQL HTTP server. When this file is run it creates an instance of ApolloServer. It takes an options object as its parameter with two required properties: typeDefs and resolvers. TypeDefs is a string representing the GraphQL schema. Resolvers is a map of functions that implement your schema.
The server is listening for connections to localhost port 4000. Actually, ApolloServer uses port 4000 by default so you could leave the port out.
You may have noticed that there is no Express. Express is the most popular web framework for Node.js. So what gives? Well, Express is there. It's a dependency of apollo-server, along with apollo-server-express, which connects apollo-server with express. Apollo-server uses them behind the scenes. For existing Express apps, if you want to keep Express in the foreground I'll show you how to do that at the end of this tutorial.
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 at http://localhost:4000/." in the terminal, and if you open a browser to localhost:4000 you should see the GraphQL Playground (more on that in a minute). To stop the server, from the terminal press CTRL+C,
We put a start script in the 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
We add this script because many production servers start the app by calling 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
Both apollo-server and express-graphql come with GUI tools that you run in your browser on your app's URL to send HTTP requests. Apollo-server's tool is called GraphQL Playground. It is similar to API request tools like Postman or Curl except it is specific to a GraphQL API.
Let's test out our Hello World app by pasting a query in the left side panel:
{ hello }
Then run it by clicking on the Execute Query button in the middle. The output should look like:
{ "data": { "hello": "world" } }
Now let's build something closer to a real app. One that uses a 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.
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 web 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. This is all the code we will use for this file, but I'll only explain the Mongoose code for now.
1 const { ApolloServer } = require('apollo-server');
1. const mongoose = require('mongoose');
3
4 const typeDefs = require('./graphql/typeDefs');
5 const resolvers = require('./graphql/resolvers');
6
2. const PORT = 4000;
3. const MONGODB_URI = "mongodb://localhost:27017/my_local_db";
9
4. mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useFindAndModify: false });
5. mongoose.connection.once('open', function() {
12 console.log('Connected to the Database.');
13 });
14 mongoose.connection.on('error', function(error) {
15 console.log('Mongoose Connection Error : ' + error);
16 });
17
18 const server = new ApolloServer({
19 typeDefs,
20 resolvers
21 });
22
23 server.listen(PORT).then(({ url }) => {
24 console.log(`Server listening at ${url}.`);
25 });
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 4000. 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.
An API paired with a dedicated client like React 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
// server ... const cors = require('cors'); ... const server = new ApolloServer({ typeDefs, resolvers, cors: true });
Install and import the cors package. Then add it as an option to the the ApolloServer instance.
For more on the CORS concept see developer.mozilla.org/en-US/docs/Web/HTTP/CORS
Populate the model file with the below:
1 const { Schema, model } = require('mongoose');
2
1. const articleSchema = new Schema({
4 title: {
5 type: String,
6 required: true
7 },
8 content: {
9 type: String,
10 required: true
11 }
12 });
13
2. module.exports = model('Article', articleSchema);
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 Apollo/GraphQL specific code in the server.js file so I'll cover it now:
// server.js const { ApolloServer } = require('apollo-server'); #1 const typeDefs = require('./graphql/typeDefs'); const resolvers = require('./graphql/resolvers'); ... const server = new ApolloServer({ #2 typeDefs, #3 resolvers });
TypeDefs is a required property when creating an ApolloServer instance. It is a string representations of GraphQL schema in GraphQL's Schema Definition Language (SDL). It is generated by the gql function. If you have multiple TypeDefs files you can put them in an Array and ApolloServer will concatenate them.
Populate the typeDefs file with the below:
1. const{ gql } = require('apollo-server');
2
2. const typeDefs = gql(`
3. type Article {
5 id: ID!
6 title: String!
7 content: String!
8 }
4. input ArticleInput {
10 title: String!
11 content: String!
12 }
5. type Query {
14 articles: [Article]
15 article(id: ID!): Article
16 }
6. type Mutation {
18 createArticle(articleInput: ArticleInput): Article
19 deleteArticle(id: ID!): Article
20 updateArticle(id: ID!, articleInput: ArticleInput): Article!
21 }
22 `)
23
7. module.exports = typeDefs;
Resolvers is the other required property after typeDefs when creating an ApolloServer instance. Resolvers is an object that maps resolvers with the types defined in typeDefs. The resolvers are grouped by category (i.e., Query, Mutation, Subscription, or custom). For each resolver the key should be the type name and the value should be a function to be executed for that type.
Each resolver is the equivalent to an endpoint in a RESTful API. Instead of an HTTP method/URL path combinations like GET example.com/articles, the client sends a named query or mutation that is equal to one of the resolver functions. The resolver functions are where we will interact with the database. In our case it is MongoDB with Mongoose.js as our ORM. Let's define these now:
1. const Article = require('../models/article');
2
3 const resolvers = {
4 Query: {
2. articles: function() {
6 return Article.find({});
7 },
8 article: function(parent, args) {
9 return Article.findById(args.id)
10 }
11 },
12 Mutation: {
13 createArticle: function(parent, args) {
14 let article = new Article(args.articleInput);
15 return article.save();
16 },
17 deleteArticle: function(parent, args) {
18 return Article.findByIdAndRemove(args.id);
19 },
20 updateArticle: function(parent, args) {
3. return Article.findByIdAndUpdate(args.id, args.articleInput, { new: true });
22 }
23 }
24 };
25
26 module.exports = resolvers;
ApolloServer includes a web interface where you can send queries and mutations to the API. We used it briefly to test the Hello World app. Let's try it again, this time we'll test out our API against the database and make sure that the CRUD operations all work.
First, if you are using a local MongoDB database, make sure it is running.
mongo
With all the changes we made it would be a good idea to restart the server.
CTRL+C
nodemon
Open the browser to localhost:4000. Note that since we are only using Apollo-Server and not Express, we have no routers. That means the API will ignore any path you tack on to the domain. Localhost:4000/this/is/a/bogus/path will also work.
In the browser you'll see the GraphQL 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 center 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 null after deleting it. There is a history tab on the top left and if you click it you should see all the queries and mutations you ran. Select the the articles query then the Use button. It will queue up the query so you can 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.
Ta Da!, you are done with the API portion of the Build a GraphQL CRUD application with Node and React tutorial. If you want to add a React front end then on to part 2 Build a React app with GraphQL Tutorial
If you want to tack GraphQL onto an existing Express app using Apollo-server most of the steps are the same as above. Just make the below changes.
In addition to the apollo-server and mongoose packages, you need to install express and apollo-server-express. Technically they are already installed as dependencies to apollo-express, but it's cleaner to install them explicitly so they get listed in package.json:
npm install express apollo-server-express
Then change the server.js file to the below. The changes are bolded.
6 const express = require('express');
7 const { ApolloServer } = require('apollo-server-express');
8 const mongoose = require('mongoose');
9
10 const typeDefs = require('./graphql/typeDefs');
11 const resolvers = require('./graphql/resolvers');
12
13 const PORT = 4000;
14 const MONGODB_URI = "mongodb://localhost:27017/my_local_db";
15
16 const app = express();
17
18 mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useFindAndModify: false });
19 mongoose.connection.once('open', function() {
20 console.log('Connected to the Database.');
21 });
22 mongoose.connection.on('error', function(error) {
23 console.log('Mongoose Connection Error : ' + error);
24 });
25
26 const server = new ApolloServer({
27 typeDefs,
28 resolvers
29 });
30
31 server.applyMiddleware({ app, path: '/graphql' });
32
33 app.listen(PORT, function() {
34 console.log(`Server listening on port ${PORT}.`);
35 });
Most of this is the standard Express setup.
On the ApolloServer side, most of this is the same. You create an instance of ApolloServer passing in typeDefs and resolvers as the options parameter. But this time you add the express app as middleware, and you set an explicit path for the graphql endpoint. The other files remain the same. That's it!