Repository navigation
Create a back end server and database
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.
Firstly, install express and bodyParser via yarn.
yarn add express body-parser
nano server.jsconst 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!"}
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 mongodbNow 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 --devThe 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.jsconst 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
}
...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" >> .envThis 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*" >> .gitignoreNote 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.exampleBecause 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' >> .gitignoreGit should be seeing your .env.example file now.
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.jsconst 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.
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.
$ curl http://localhost:8080/contacts
[]
$ 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.
$ curl http://localhost:8080/contacts/5898a2b26868700d374f24cf
{"name":"Jules","email":"jules@example.com","phone":"11111","id":"5898a2b26868700d374f24cf"}
$ 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"}]
$ 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