Project Setup

This tutorial assumes you are familiar with JavaScript, know how to use the command line, are familiar with Git, 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 node-api-tutorial (or whatever you want to call it).

  • mkdir node-api-tutorial && cd node-api-tutorial
  • Create our first file (we'll populate it later):

  • touch server.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 us to do hot reloading of our app, which means it will restart the server every time we make a change and save it in our project. Use the --global flag (or -g short version). 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 our project, in the node_modules folder. Let's install the following packages:

  • npm install express mongoose cors
  • A list of the project's installed packages with version and their dendencies is saved to the package-lock.json file.

    Add a script to our package.json file to run the server. Our package.json file will look something like the below. We can run command in the scripts object from the CLI with npm run script-name. I'll explain more in the next section.

    // package.json
    {
      "name": "node-api-tutorial",
      "version": "1.0.0",
      "description": "Web API built with Node.js, Express, and MongoDB",
      "main": "server.js",
      "dependencies": {
        "cors": "^2.8.5",
        "express": "^4.17.1",
        "mongoose": "^5.6.3"
      },
      "devDependencies": {},
      "scripts": {
        "start": "node server.js",
      },
      "keywords": [],
      "author": "",
      "license": "MIT"
    }
    

    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('/', (req, res) => res.send('Hello World!'));
    
    app.listen(PORT, () => console.log(`Server listening on port ${PORT}`));
    

    Execute this file with the node command:

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

    We made a script to run this command in our package.json file. Call it with:

  • npm run start
  • Actually start is one of a few special cases where "run" is optional so we can run it with just:

  • npm start
  • We installed nodemon for hot reloading so to use that we just run nodemon and it looks for a file named server.js by default.

  • 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. If you follow along to part 3 where we deploy to Heroku then you need the cloud solution. I will explain both ways.

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

    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 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. -R flag means make 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.

  • sudo chown -R $(whoami) data/db
  • 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 have that window open and running to access the database:

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

    MongoDB Shell

    Access the MongoDB shell from the Terminal:

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

  • show dbs - Returns a list of your databases.
  • use dbname - Use the specified database.
  • db - Returns the db you are currently in.
  • show collections - Returns the collections in the db you are currently in.
  • db.createCollection(name, options) - Create a collection.
  • db.collection-name.find() - Show all documents in the collection.
  • db.collection-name.insert( { field: value, field: value } ) - Insert a document into a collection. Put value in double quotes if it is a string.
  • db.articles.insert({title: "Learn Node.js", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."})
  • 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 = "Your MongoDB Atlas link goes here";       #4a
    //const MONGODB_URI = "mongodb://localhost:27017/my_local_db"; #4b
    
    app.use(cors());                                               #5
    app.use(express.urlencoded({ extended: true }));               #6
    app.use(express.json());
    app.use('/api', router);                                       #7
    
    mongoose.connect(MONGODB_URI, { useNewUrlParser: true });      #8
    mongoose.connection.once('open', () => {                       #9
      console.log('Connected to the Database.');
    });
    mongoose.connection.on('error', err => {
      console.log('Mongoose Connection Error : ' + err);
    });
    
    app.listen(PORT, () => {                                       #10
      console.log(`Server listening on port ${PORT}.`);
    });
    

    Schema and Model

    Make a folder and file for our article model:

  • mkdir models && touch models/article.js
  • // 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 URI (or path) and a specific 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 for adding (Create) a new article, editing (Update), and deleting existing articles. So our routes will be "RESTful" which essentially means our application routes and actions are build around performing these database CRUD actions.

    Make a folder and file for our routes:

  • mkdir routes && touch routes/index.js
  • // 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 and we'll add on to that a JSON response with the new article object that 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 deletes 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);
    

    Test your API with Curl or Postman

    Curl

    We are done with our simple API. Well almost. We need to make sure it works. We were able to test the GET request in the browser by just entering the URL. But how do we test the POST, PATCH and DELETE requests without having our front end built yet? We want to make sure it works before we move on. Well there are 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 JavaScript","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 id, created_at, and updated_at fields. You can also make a Curl GET request with the new ID:

    curl -X GET http://localhost:3001/api.articles/article_id - change the id to the correct one if it's not 4.

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

    curl -H "Content-Type: application/json" -X PATCH -d '{"title":"Learn ES6","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, go to part 2: MERN APP: Integrate React.