Project Setup

In this 2-part tutorial series we will build a simple articles app 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.

This tutorial assumes you are familiar with JavaScript, know how to use the command line, and have Node.js installed on your computer. First your version of Node.js should be recent. To see your version, go to the command line and run node -v (returning a version means node is at least installed). Compare that to the latest LTS version from nodejs.org and upgrade if it's a major release (first digit) behind for sure.

Create a directory for your project and cd into it. We'll call it mern-app-tutorial (or whatever you want to call it).

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

    touch server.js
    mkdir models
    touch models/article.js
    mkdir routes
    touch routes/index.js
    

    Package.json

    Generate a package.json file. This file contains the metadata for a Node.js project. Adding the --yes flag 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 your 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
  • 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-tutorial",
      "version": "1.0.0",
      "description": "Web app built with Node.js, Express, MongoDB, and React",
      "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",
        "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. 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
  • A quick 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. You have the option of installing Babel which will translate it for you. Since it's not critical to this tutorial we won't do it. Also, as default I am using the declaration syntax for functions rather than arrow functions. That just makes it consistently explicit what your function is returning or if it isn't returning anything.


    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.

    // server.js
    const express = require('express');                          #1
    const mongoose = require('mongoose');
    // const cors = require('cors');
    const router = require('./routes/index');
    
    const app = express();                                       #2
    const PORT = 3001;                                           #3
    const MONGODB_URI = "mongodb://localhost:27017/my_local_db"; #4
    
    // app.use(cors())
    app.use(express.urlencoded({ extended: true }));             #5
    app.use(express.json());
    app.use('/api', router);                                     #6
    
    mongoose.connect(MONGODB_URI, { useNewUrlParser: true, useFindAndModify: false });    #7
    mongoose.connection.once('open', function() {                #8
      console.log('Connected to the Database.');
    });
    mongoose.connection.on('error', function(error) {
      console.log('Mongoose Connection Error : ' + error);
    });
    
    app.listen(PORT, function() {                                #9
      console.log(`Server listening on port ${PORT}.`);
    });
    
    1. Start by importing our express and mongoose packages and our Express router which we'll define shortly.
    2. Calling the express() function will create our running app object.
    3. 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 to give us the flexibility to use different ports depending on whether we are in development or production.
    4. Assigning constants for things like port number and the database URL gives us the flexibility to change the values in one place.
      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. Chaining Express's use method to our app object gives us access to the libraries we imported. express.urlencoded({ extended: true }) and express.json() are middleware for parsing requests with JSON payloads (for POST and PATCH/PUT requests).
    6. Apply the Express router object to your Express app. I'll explain this in the routing section.
    7. mongoose.connect() connects to our MongoDB database
    8. Optionally, log a message if the above connection was successful and one if it is not.
    9. 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. There is an npm package called cors that will apply additional HTTP headers to tell the browser to allow access from a specified origin or any origin.

    A standard MERN app is really an integrated single web application even if the back end and front end run independently. We only want to give access to the integrated front end (our React client in this case) and therefore won't use the cors package. There's an additional step when we build the React app that we'll cover there.

    If, however, your API is meant 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

    Schema and Model

    Populate the model file with the below:

    // models/article.js
    
    const mongoose = require('mongoose');
    
    const articleSchema = new mongoose.Schema({                #1
      title: {
        type: String,
        required: [true, "Title is required"]
      },
      content: {
        type: String,
        required: [true, "Content can't be blank"]
      }
    });
    
    module.exports = mongoose.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. Read about Mongoose Schema at 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.

    Routes

    Now that we have our server, database and model set up, the last step for our simple API application is routing. Routing refers to determining how an application responds to a client request to a particular endpoint. An endpoint is a combined URI and HTTP request method (GET, POST, PATCH or PUT, DELETE) that activates specific actions from our API.

    We will apply database Create, Read, Update, and Delete (CRUD) actions to each endpoint. Meaning we will have an endpoint to return (Read) a JSON array of all our articles and another endpoint to return (Read) a JSON object for a specified article id. We will also have endpoints to Create, Update, and Delete articles. So our routes will be "RESTful" which essentially means our application routes and actions are build around performing these database CRUD actions.

    Populate the routes file:

    // routes/index.js
    
    const express = require ('express'); 
    const router = express.Router();                  #1
    const Article = require('../models/article');     #2
    
    router.get('/articles', function(req, res) {      #3
      Article.find(function(err, articles) {
        res.json(articles);
      });
    });
    
    router.get('/articles/:id', function(req, res) {  #4
      Article.findById(req.params.id, function(err, article) {
        if (!article) {
          res.status(404).send('No result found');
        } else {
          res.json(article);
        }
      });
    });
    
    router.post('/articles', function(req, res) {     #5
      let article = new Article(req.body);
      article.save()
        .then(article => {
          res.send(article);
        })
        .catch(function(err) {
          res.status(422).send('Article add failed');
        });
    });
    
    router.patch('/articles/:id', function(req, res){    #6
      Article.findByIdAndUpdate(req.params.id, req.body)
        .then(function() {
          res.json('Article updated');
        })
        .catch(function(err) {
          res.status(422).send("Article update failed.");
        });
    });
    
    router.delete('/articles/:id', function(req, res) {  #7
      Article.findById(req.params.id, function(err, article) {
        if (!article) {
          res.status(404).send('Article not found');
        } else {
          Article.findByIdAndRemove(req.params.id)
            .then(function() { res.status(200).json("Article deleted") })
            .catch(function(err) {
              res.status(400).send("Article delete failed.");
            })
        }
      });
    })
    
    module.exports = router;                             #8
    
    1. Create an instance of the Express Router to be used as middleware for our routes.
    2. Import the Article model.
    3. For each API endpoint we will chain a method to the router object. The format is:
      router.HTTP Method(path, handler function)
      We imported the Article model representing the articles collection in our database. We chain methods from the mongoose library to the Article prototype that will perform different types of CRUD actions. Our handler functions perform the CRUD operation and may return a response. Generally only return responses that will be used by the client.

    4. Get request to /articles returns a JSON array of all article objects found in the database.
    5. Get request to /articles/:id (:id is a variable representing an article's _id) returns a JSON object of the specified article if it exists, otherwise returns status 404 and "No result found"
    6. Post request to /articles creates a new Article instance from the JSON object in sent in the HTTP request body and saves it to the database. If successful a status 200 code is automatically returned. We'll add on to that a JSON response with the new article object we just added which includes the article _id generated by the database.
    7. Patch request to /articles/:id updates the specified article with the JSON object sent in the HTTP request body. You could use the PATCH, PUT or POST HTTP methods since they all send a payload. It's the handler function that determines what is done with the payload. On a successful update we are returning a JSON response just stating "Article updated". If the article did not update then we send an Unprocessable Entity code 422 response with a message.
    8. Delete request to /articles/:id first checks if the article exists. If so it deletes it and sends status 200 with a JSON response of "Article deleted".
    9. Export the router object with our Article endpoints.

    In the server.js file we imported our router object and then chained it to our Express app object. The first argument '/api' applies our router object when the '/api' path is called.

    // server.js
    const router = require('./routes/index');
    ...
    app.use('/api', router);
    

    If you are familiar with the Model-View-Controller pattern (MVC) commonly used by web app frameworks, Express uses that pattern as well. Model we talked about. We are combining the controller actions directly with the routes rather than separating them into separate route and controller files. The view will be our React front end. Or you may say our view is the JSON responses returned when accessed by our front end React app. Let's see those in action.

    With all the changes it's best to just restart your server:

  • CTRL+C
  • nodemon
  • Then in your browser go to the API articles endpoint:
    http://localhost:3001/api/articles
    And you should see an empty array. Congratulations! All that hard work paid off. An empty array. Now lets test all the endpoints using Curl and/or Postman.


    Test your API with Curl or Postman

    Curl

    We're not done with our API until we know all the endpoints we created work. I'll discuss two ways we can do this. First is using Curl.

    Curl is a command line utility that lets you make HTTP requests from the Terminal instead of from a web page. Curl comes preloaded with Macs. For Windows you may need to install it, but it's a handy tool sometimes. Let's make a POST request by entering the below Curl command. The -H option is for the header which is where we tell the receiver that the data we are sending is in JSON format. The -X option specifies the request method, POST in this case. The -d option is where we put our data, so you won't use the -d option with GET or DELETE requests. And of course we include the URL.

  • nodemon Make sure you start the server if it's not already running
  • curl -H "Content-Type: application/json" -X POST -d '{"title":"Learn MERN","content":"Lorem Ipsum."}' http://localhost:3001/api/articles

    If it posted the server should send a response back with the new Article data in JSON format. It will have the title and content fields as you submitted, but it will also have an autogenerated _id field. If you go back to your browser localhost:3001/api/articles and refresh it, your empty array should now have one article in it.

    You can also make a Curl GET request with the new ID:

    curl -X GET http://localhost:3001/api/articles/article._id - inserting the _id.

    Now let's update this record with a PATCH request:

    curl -H "Content-Type: application/json" -X PATCH -d '{"title":"Learn the MERN Stack","content":"Lorem ipsum."}' http://localhost:3001/api/articles/article_id

    It returns a JSON string of the saved updated article data. Now, let's delete it:

    curl -X DELETE http://localhost:3001/api/articles/article._id


    Postman

    If you don't like the cryptic feel of sending HTTP requests from the command line, there is a popular free desktop application called Postman that lets you do the same thing from a GUI interface. You can download it from getpostman.com. The tool is fairly intuitive but you will need to play around with it a bit to get comfortable with it. If you plan on building a lot of APIs Postman is an essential tool. Here are the CRUD actions hitting our API's endpoints:


    If everything works then our simple API is done! We can use this API as the back end for a mobile app, or to be accessed by other websites to display our fabulous articles. Or we could integrate it with a front end library like React to make a fully functioning web application. A so-called single-page-app (SPA). To do that last one, go on to part 2: MERN APP: Integrate React.