Skip to content

Create a back end server and database

Pamela Argentino edited this page Feb 23, 2017 · 14 revisions

Now that our app is mostly done, except it doesn't have data persistence, it's about time we implement it. For that, we will use the Express.js Node library that makes it easy to set up a server and MongoDB as the database engine.

Create an HTTP server with Express

Firstly, install express and bodyParser via yarn.

yarn add express body-parser
nano server.js
const express = require('express');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const server = app.listen(process.env.PORT || 8080, () => {
  const { port } = server.address();
  console.log('App now running on port %d in %s mode', port, app.settings.env);
});

app.get('/', (req, res) => {
  res.status(200).json({ message: 'Hello World!' });
});

Note that we haven't used the import statement. This is because the back-end server is going to run on server side Node.js without Babel or Webpack, and Node.js currently doesn't support import statements.

To check whether this works, run node server.js and, in another terminal tab, curl http://localhost:8080.

$ curl http://localhost:8080/
{"message":"Hello World!"}

Installing MongoDB

To install MongoDB on your local machine, follow the instructions here.

Once MongoDB is installed, check if the command mongo exists by running which mongo in your terminal.

$ which mongo
/usr/local/bin/mongo

The bin path may vary, but it shouldn't come up empty. If it exists, run mongo on the terminal.

$ mongo
MongoDB shell version: [your MongoDB version]
connecting to: test
>

Once the MongoDB console is up, create a new MongoDB database by running use contacts. If the contacts collection doesn't exist, it will be created.

> use contacts
switched to db contacts

Exit the console, then install the Node.js MongoDB client to your app via yarn.

yarn add mongodb

Configuring .env to your app

Now that we're on the database configuration step, we need a way to safely store and switch database keys between environments (dev, staging, production) while also keeping those out of source control, as dictates the Twelve-Factor App tenets.

To use .env in Node.js, we need to install the dotenv library via yarn.

yarn add dotenv --dev

The reason we're installing this as a dev dependency is that we're going to deploy our app to Heroku and it doesn't need a .env file at all to work.

Once the dotenv lib is installed, load it up on server.js wrapped in a try-catch block. The reason why we will do it is because dotenv won't be installed in production and the require will fail and break the app.

nano server.js
const express = require('express');
const bodyParser = require('body-parser');

try {
  const env = require('dotenv');

  env.config({ silent: true });
}
catch(e) {
  // Do nothing - if dotenv is not installed then we don't really need it
}
...

Set the database connection string to .env

In a local connection such as the one we're setting up for our app in development, the connection string is as simple as mongodb://localhost:27017/contacts, the port being whatever port MongoDB is running on in your workstation. Let's set it up to our .env file.

echo "MONGODB_URI=mongodb://localhost:27017/contacts" >> .env

This will create a .env file containing a line of text with MONGODB_URI=mongodb://localhost:27017/contacts, which is the syntax for creating environment variables in most operating systems.

It's imperative that .env does NOT get checked in to source control. To ignore it on Git, add it to your .gitignore file.

echo ".env*" >> .gitignore

Note that we have added an asterisk to the end of .env. This is because we might want to keep track of multiple environments (testing, staging, production) and none of those should be committed to source control. This way, we can create different .env files, such as .env.testing, .env.staging, .env.production without running the risk of accidentally adding it to the repo.

It's also good practice to commit to source control a .env.example file containing all of the keys needed for the app, sans their values. This way, other developers in your team or contributors to an open source project know which env variable names are being used in the code. Remember to always keep .env.example updated whenever you add a new env variable to .env as well.

echo "MONGODB_URI=" >> .env.example

Because we have added all sorts of .env files to the ignore list, .env.example will be ignored by Git as well. To add an exception to .gitignore, we list the filename with an exclamation mark (!) prepending it (e.g. !.env.example). But we can't append it via command line like we were doing, because terminals interpret anything with a leading ! as an event and simply echoing to the terminal's stdout will fail. So let's do it the hard way: open a text editor and add it manually.

echo '!.env.example' >> .gitignore

Git should be seeing your .env.example file now.

Set up the database connection on the Express server

Now that we have dotenv set up, as well as an .env file, we're ready to prep up a database connection in our backend server. The .env vars are accessible through the process.env Node.js object. This means that our MongoDB connection string is available to the server through process.env.MONGODB_URI.

Since the connection process is asynchronous, we will wrap the Express listen call into the MongoDB connection callback. This way we ensure that the server will always have a connection up.

nano server.js
const express = require('express');
const bodyParser = require('body-parser');
const { MongoClient } = require('mongodb');
...
app.use(bodyParser.json());

var db; // We will declare the database connection variable outside of the callback
        // so that the server gets global access to it

MongoClient.connect(process.env.MONGODB_URI, (error, database) => {
  if (error) {
    console.log('There was an error while trying to connect to the database.', error);
    process.exit(1);
  }

  console.log('Successfully connected to MongoDB.');

  db = database;

  const server = app.listen(process.env.PORT || 8080, () => {
    const { port } = server.address();
    console.log('App now running on port %d in %s mode', port, app.settings.env);
  });
});
...
$ node server.js
Successfully connected to MongoDB.
App now running on port 8080 in development mode

Now we have an operational Node.js back-end with a database connection. Let's put it to good use.

Configuring the API routes

We will need API routes for each CRUD operation: create, retrieve, update and delete. The structure we will use in this guide is the following:

GET /contacts - list all contacts
GET /contacts/:id - fetch contact by id
POST /contacts - insert new contact
PUT /contacts/:id - update contact
DELETE /contacts/:id - delete contact

The Express object has a function for each of those methods. Let's wire them up.

nano server.js
...
const { MongoClient, ObjectID } = require('mongodb');
...
var db; // We will declare the database connection variable outside of the callback so that the server gets global access to it
const COLLECTION = "contacts";
...
app.get('/contacts', (req, res) => {
  db.collection(COLLECTION).find({}).toArray((error, docs) => {
    if (error) {
      handleError(res, error.message, "Failed to fetch contacts"); // If this fails, return an Error 500 - Internal Server Error
    } else {
      let contacts = [];

      docs.forEach(doc => {
        contacts.push({
          id: doc._id,
          name: doc.name,
          phone: doc.phone,
          email: doc.email
        });
      });

      res.status(200).json(contacts);
    }
  })
});

app.get('/contacts/:id', (req, res) => {
  db.collection(COLLECTION).findOne({ _id: new ObjectID(req.params.id) }, (error, doc) => {
    if (error) {
      handleError(res, error.message, 'Failed to fetch contact', 404);
    } else {
      res.status(200).json({
        id: doc._id,
        name: doc.name,
        phone: doc.phone,
        email: doc.email
      });
    }
  })
});

app.post('/contacts', (req, res) => {
  const { body }  = req;

  if (!(body.name || body.email)) {
    handleError(res, "Invalid user input", "Must provide name and e-mail at least.", 400);
    return;
  }

  db.collection(COLLECTION).insertOne(body, (error, doc) => {
    if (error) {
      handleError(res, error.message, 'Failed to create contact');
    } else {
      res.status(201).json(doc.ops[0]);
    }
  })
});

app.put('/contacts/:id', (req, res) => {
  let { body } = req;

  delete body.id;

  db.collection(COLLECTION).updateOne({ _id: new ObjectID(req.params.id) }, body, (error, doc) => {
    if (error) {
      handleError(res, error.message, 'Failed to update contact');
    } else {
      res.status(204).end();
    }
  });
});

app.delete('/contacts/:id', (req, res) => {
  db.collection(COLLECTION).deleteOne({ _id: new ObjectID(req.params.id) }, (error, result) => {
    if (error) {
      handleError(res, error.message, "Failed to delete contact");
    } else {
      res.status(204).end();
    }
  })
});

const handleError = (res, reason, message, code) => {
  console.log('Error: %s', reason);
  res.status(code || 500).json({ error: message });
};

Let's try it out. Run node server.js on your terminal, open a new tab then run curl against the new API routes.

Listing contacts from the database

$ curl http://localhost:8080/contacts
[]

Inserting a new contact, then listing

$ curl -X POST \
  -H "Content-Type: application/json" \
  --data '{"name": "Jules", "email": "jules@example.com", "phone": "11111"}' \
  http://localhost:8080/contacts

  {"name":"Jules","email":"jules@example.com","phone":"11111","_id":"5898a2b26868700d374f24cf"}

$ curl http://localhost:8080/contacts
[{"id":"5898a2b26868700d374f24cf","name":"Jules","phone":"11111","email":"jules@example.com"}]

Note that, when we fetch the contacts via GET, the objects returned come with an id key, instead of _id. This is because we have adapted the fetched object structure to comply with what our React app has right now, which doesn't implicate in how the data is stored in MongoDB.

Listing a contact by its ID

$ curl http://localhost:8080/contacts/5898a2b26868700d374f24cf
{"name":"Jules","email":"jules@example.com","phone":"11111","id":"5898a2b26868700d374f24cf"}

Updating existing contact, then listing

$ curl -X PUT \
  -H "Content-Type: application/json" \
  --data '{"name": "Julian", "phone": "22222", "email": "julian@example.com"}' \
  http://localhost:8080/contacts/5898a2b26868700d374f24cf
$ curl http://localhost:8080/contacts
[{"id":"5898a2b26868700d374f24cf","name":"Julian","phone":"22222","email":"julian@example.com"}]

Deleting existing contact, then listing

$ curl -X DELETE http://localhost:8080/contacts/5898a2b26868700d374f24cf
$ curl http://localhost:8080/contacts
[]

Our back-end API is fully tested and ready to go. Commit your changes and head to the next step.

git add .
git commit -m "Create back-end server and configure MongoDB database"

Next step: Integrate back-end server to React App

Clone this wiki locally