Deploy a MERN stack app on Heroku

2018-10-12

In this article, I will describe how to take an existing Web Application that is build using MongoDB, ExpressJS, Node.js, and React (often called as MERN stack) on a deployment service like Heroku. If you have an existing application built using the same tech stack, you can definitely skip the process in which I show you to quickly build a web application and go straight on to the deployment part. For the rest of you, please continue to read.

MERN Stack

MongoDB, ExpressJS, Node.js, and Reactjs are used together to build web applications. In this, Node.js and Express bind together to serve the backend, MongoDB provides a NoSQL database to store the data and frontend is built using React that a user interacts with. All four of these technologies are open source, cross-platform and JavaScript based. Since they are JavaScript based, one of the main reasons why they are often used together.

As JavaScript is used throughout to build a Fullstack application, developers do not need to learn and change the context of using different programming languages to build or work on different aspect of a web application.

To continue to follow this tutorial there are requirements that you will need to build the demo application and then deploy it on Heroku.

  • Node.js/npm installed
  • Heroku account

For MongoDB, we are going to use a cloud based service like mLab which provides database as a service. It has a free tier, and having an account there will be time saving.

Building a Full-stack app using MERN

Building the Backend

I am going to take you through building a web application using MERN Stack. To start, please create an empty directory and type the following commands in the order they are specified.

1# to generate package.json
2npm init -y
3
4# install following dependencies
5npm install -S express mongoose
6
7npm install -D nodemon

Create a server.js file inside the root of the directory. This file will server us the backend file for us.

1// server.js
2const express = require('express')
3const app = express()
4
5const PORT = process.env.PORT || 5000
6
7// configure body parser for AJAX requests
8app.use(express.urlencoded({ extended: true }))
9app.use(express.json())
10
11// routes
12app.get('/', (req, res) => {
13 res.send('Hello from MERN')
14})
15
16// Bootstrap server
17app.listen(PORT, () => {
18 console.log(`Server listening on port ${PORT}.`)
19})

Now, I made following changes in package.json for this program to work.

1"main": "server.js",
2 "scripts": {
3 "server": "nodemon server.js",
4 }

To see if everything is working, run the command npm start server that we just defined in package.json as a script. If there are no errors, you will get the following result. Visit the following url: [http://localhost:5000](http://localhost:5000.).

Please note that onwards Express version 4.16.0 body parser middleware function is a built-in middleware and there is no need to import it as a separate dependency. Body parser middleware is required to handle incoming AJAX requests that come in the form of JSON payloads or urlencoded payloads.

Models with Mongoose

When I am not writing JavaScript, I am a bibliophile. Thus, for this demonstration, I am going to build a web application that tends to take care of all the books that I want to read. If you are into books, you can think of it is as your own personal TBR manager.

I will start by creating a database model called Book inside the file models/Books.js. This will resemble a schema of what to expect from the user when adding information to our application.

1// Books.js
2const mongoose = require('mongoose')
3const Schema = mongoose.Schema
4
5const bookSchema = new Schema({
6 title: {
7 type: String,
8 required: true
9 },
10 author: {
11 type: String,
12 required: true
13 }
14})
15
16const Book = mongoose.model('Book', bookSchema)
17
18module.exports = Book

I am using mongoose to define the schema above. Mongoose is an ODM (Object Document Mapper). It allows you to define objects with a strongly typed schema that is mapped as a MongoDB collection. This schema architecture allows us to provide an organized shape to the document inside the MongoDB collection.

In our bookSchema we are defining two fields: a title which indicates the title of the book and an author representing the name of the author of the book. Both these fields are string type.

Defining Routes

Our application is going to need some routes that will help the client app to communicate with the server application and perform CRUD (Create, Read, Update, Delete) operations. I am defining all the business logic that works behind every route in a different file. Conventionally, named as controllers. Create a new file controllers/booksController.js.

1// booksControllers.js
2const Book = require('../models/Books')
3
4// Defining all methods and business logic for routes
5
6module.exports = {
7 findAll: function(req, res) {
8 Book.find(req.query)
9 .then(books => res.json(books))
10 .catch(err => res.status(422).json(err))
11 },
12 findById: function(req, res) {
13 Book.findById(req.params.id)
14 .then(book => res.json(book))
15 .catch(err => res.status(422).json(err))
16 },
17 create: function(req, res) {
18 Book.create(req.body)
19 .then(newBook => res.json(newBook))
20 .catch(err => res.status(422).json(err))
21 },
22 update: function(req, res) {
23 Book.findOneAndUpdate({ _id: req.params.id }, req.body)
24 .then(book => res.json(book))
25 .catch(err => res.status(422).json(err))
26 },
27 remove: function(req, res) {
28 Book.findById({ _id: req.params.id })
29 .then(book => book.remove())
30 .then(allbooks => res.json(allbooks))
31 .catch(err => res.status(422).json(err))
32 }
33}

The business logic or you can say the controller logic behind the application is nothing but the methods that will work on a specific route. There are five functions in total. Each has its own use. I am requiring our Book model, previously created, as it provides functions for us to query CRUD operations to the database. A mongoose query can be executed in two ways, by providing a callback function or by using .then() function which also indicates that mongoose support promises. I am using the promising approach above to avoid the nuisance caused by nested callbacks (and commonly known as callback hell).

Next step is to use these methods in our routes inside routes/ directory. Create a new file called books.js.

1// books.js
2
3const router = require('express').Router()
4const booksController = require('../controllers/booksController')
5
6router
7 .route('/')
8 .get(booksController.findAll)
9 .post(booksController.create)
10
11router
12 .route('/:id')
13 .get(booksController.findById)
14 .put(booksController.update)
15 .delete(booksController.remove)
16
17module.exports = router

I have separated the concerned routes that match a specific URL. For example, routes that are starting with :id routing parameter are defined above together in the file. Open index.js in the same directory and add the following.

1// index.js
2
3const router = require('express').Router()
4const bookRoutes = require('./books')
5
6router.use('/api/books', bookRoutes)
7
8module.exports = router

I am adding a prefix /api/books before the routes. This way, you can only access them as [http://localhost:5000/api/books](http://localhost:5000/api/books.).

For this to work, I am going to import book routes in the server.js file after every other middleware defined and before we have bootstrapped the server.

1// server.js
2
3const routes = require('./routes')
4
5// after all middleware functions
6
7app.use(routes)

Also remove the default route app.get('/')... that was previously created. We are soon going to serve the application's front end here on the default route.

Connecting with Database using mLab

I am going to use mlab to host the database of our application on the cloud. Once you create an account, your dashboard will look similar to mine. I already have few sandboxes running, so do not mind them.

To create a new one, click on the button Create New under MongoDB deployments. After that, you select the plan type Sandbox which provides the free tier up to 500MB.

After the MongoDB deployment is created, a database user is required by the mlab to have you connect to this database. To create one now, visit the ‘Users’ tab and click the ‘Add database user’ button.

Now copy the string provided by mlab such as:

mongodb://<dbuser>:<dbpassword>@ds125453.mlab.com:25453/mern-example

and add the dbuser and dbpassword you just entered to create the new user. I am going to save these credentials as well as the string given by mlab to connect to the database inside a file called config/index.js.

1// config/index.js
2const dbuser = 'xxxxxxxxxx'
3const dbpassword = 'xxxxxxxxx'
4
5const MONGODB_URI = `mongodb://${dbuser}:${dbpassword}@ds125453.mlab.com:25453/mern-example`
6
7module.exports = MONGODB_URI

You can replace the x's for dbuser and dbpassword. Now to define the connection with mlab string we are again going to use mongoose. Create a new file inside models/index.js.

1// models/index.js
2
3const mongoose = require('mongoose')
4const URI = require('../config/index')
5
6mongoose.connect(process.env.MONGODB_URI || URI)
7
8// When successfully connected
9mongoose.connection.on('connected', () => {
10 console.log('Established Mongoose Default Connection')
11})
12
13// When connection throws an error
14mongoose.connection.on('error', err => {
15 console.log('Mongoose Default Connection Error : ' + err)
16})

We are importing the same database URI string that we just exported in config. I am going to require this file inside our server.js before any middleware is defined.

1// server.js
2const express = require('express')
3const app = express()
4
5const routes = require('./routes')
6
7const PORT = process.env.PORT || 5000
8
9// require db connection
10require('./models')
11
12// configure body parser for AJAX requests
13app.use(express.urlencoded({ extended: true }))
14app.use(express.json())
15
16app.use(routes)
17
18// Bootstrap server
19app.listen(PORT, () => {
20 console.log(`Server listening on port ${PORT}.`)
21})

Now run the server again and if you get the following message, that means your database is gracefully connected to the web server.

Building the FrontEnd with React

To build the user interface of our application, I am going to create-react-app. Run the following command to generate a react application inside a directory called client.

1create-react-app client/

Once the scaffolding process is complete, run npm run start after traversing inside the client directory from your terminal, and see if everything works or not. If you get a screen like below that means everything is top-notch.

Install two dependencies from npm that we need to in order for the client to work.

1yarn add axios react-router-dom@4.1.2

You are going to need axios to make AJAX requests to the server. react-router-dom is for switching between navigation routes.

I am not going to walk you through every component and reusable component I have built in this application. I am only going to take you through what needs to be done connect the React app to Node.js server, the build process and then deploying it.

The main frontend file, App.js looks like this:

1import React from 'react'
2import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
3import Books from './pages/Books'
4import Detail from './pages/Detail'
5import NoMatch from './pages/NoMatch'
6import Nav from './components/Nav'
7
8const App = () => (
9 <Router>
10 <div>
11 <Nav />
12 <Switch>
13 <Route exact path="/" component={Books} />
14 <Route exact path="/books" component={Books} />
15 <Route exact path="/books/:id" component={Detail} />
16 <Route component={NoMatch} />
17 </Switch>
18 </div>
19 </Router>
20)
21
22export default App

Next, I have created an API.js inside the utils directory which we handle all the requests and fetching data, in simple terms AJAX requests between our client and the server.

1import axios from 'axios'
2
3export default {
4 // Gets all books
5 getBooks: function() {
6 return axios.get('/api/books')
7 },
8 // Gets the book with the given id
9 getBook: function(id) {
10 return axios.get('/api/books/' + id)
11 },
12 // Deletes the book with the given id
13 deleteBook: function(id) {
14 return axios.delete('/api/books/' + id)
15 },
16 // Saves a book to the database
17 saveBook: function(bookData) {
18 return axios.post('/api/books', bookData)
19 }
20}

We also have pages and a separate components directory. The pages contain those files that are going to display the content when we add a book and its author in our list using a form to submit the data to the backend. The form itself uses different reusable components which are built separately. The sole purpose of doing this is to follow best practices that are convenient to understand the source code and a common practice in the React community.

There are two pages Books and Details. Let us go through them.

1// Books.js
2
3import React, { Component } from 'react'
4import DeleteBtn from '../../components/DeleteBtn'
5import Jumbotron from '../../components/Jumbotron'
6import API from '../../utils/API'
7import { Link } from 'react-router-dom'
8import { Col, Row, Container } from '../../components/Grid'
9import { List, ListItem } from '../../components/List'
10import { Input, FormBtn } from '../../components/Form'
11
12class Books extends Component {
13 state = {
14 books: [],
15 title: '',
16 author: ''
17 }
18
19 componentDidMount() {
20 this.loadBooks()
21 }
22
23 loadBooks = () => {
24 API.getBooks()
25 .then(res => this.setState({ books: res.data, title: '', author: '' }))
26 .catch(err => console.log(err))
27 }
28
29 deleteBook = id => {
30 API.deleteBook(id)
31 .then(res => this.loadBooks())
32 .catch(err => console.log(err))
33 }
34
35 handleInputChange = event => {
36 const { name, value } = event.target
37 this.setState({
38 [name]: value
39 })
40 }
41
42 handleFormSubmit = event => {
43 event.preventDefault()
44 if (this.state.title && this.state.author) {
45 API.saveBook({
46 title: this.state.title,
47 author: this.state.author
48 })
49 .then(res => this.loadBooks())
50 .catch(err => console.log(err))
51 }
52 }
53
54 render() {
55 return (
56 <Container fluid>
57 <Row>
58 <Col size="md-6">
59 <Jumbotron>
60 <h1>What Books Should I Read?</h1>
61 </Jumbotron>
62 <form>
63 <Input
64 value={this.state.title}
65 onChange={this.handleInputChange}
66 name="title"
67 placeholder="Title (required)"
68 />
69 <Input
70 value={this.state.author}
71 onChange={this.handleInputChange}
72 name="author"
73 placeholder="Author (required)"
74 />
75
76 <FormBtn
77 disabled={!(this.state.author && this.state.title)}
78 onClick={this.handleFormSubmit}
79 >
80 Submit Book
81 </FormBtn>
82 </form>
83 </Col>
84 <Col size="md-6 sm-12">
85 <Jumbotron>
86 <h1>Books On My List</h1>
87 </Jumbotron>
88 {this.state.books.length ? (
89 <List>
90 {this.state.books.map(book => (
91 <ListItem key={book._id}>
92 <Link to={'/books/' + book._id}>
93 <strong>
94 {book.title} by {book.author}
95 </strong>
96 </Link>
97 <DeleteBtn onClick={() => this.deleteBook(book._id)} />
98 </ListItem>
99 ))}
100 </List>
101 ) : (
102 <h3>No Results to Display</h3>
103 )}
104 </Col>
105 </Row>
106 </Container>
107 )
108 }
109}
110
111export default Books

We are defining a local state to manage data and pass it on to the API from the component. Methods like loadBooks are making AJAX requests through the API calls we defined inside utils/API.js. Next is the details page.

1// Details.js
2import React, { Component } from 'react'
3import { Link } from 'react-router-dom'
4import { Col, Row, Container } from '../../components/Grid'
5import Jumbotron from '../../components/Jumbotron'
6import API from '../../utils/API'
7
8class Detail extends Component {
9 state = {
10 book: {}
11 }
12
13 componentDidMount() {
14 API.getBook(this.props.match.params.id)
15 .then(res => this.setState({ book: res.data }))
16 .catch(err => console.log(err))
17 }
18
19 render() {
20 return (
21 <Container fluid>
22 <Row>
23 <Col size="md-12">
24 <Jumbotron>
25 <h1>
26 {this.state.book.title} by {this.state.book.author}
27 </h1>
28 </Jumbotron>
29 </Col>
30 </Row>
31
32 <Row>
33 <Col size="md-2">
34 <Link to="/">Back to Authors</Link>
35 </Col>
36 </Row>
37 </Container>
38 )
39 }
40}
41
42export default Detail

It shows the books I have added in my list. To use it, first we are going to connect it with Node.js.

Connecting React and Node

There are two build steps we have to undergo through in making a connection between our client side and server side. First, open the package.json file inside the client directory and enter a proxy value that points to the same URL on which server is serving the API.

1"proxy": "http://localhost:5000"

Next step is to run the command yarn build inside the client directory such that it builds up the project. If you haven't run this command before in this project, you will notice a new directory suddenly appears.

We also need to make two changes to our backend, to serve this build directory. The reason we are doing this is to deploy our full stack application later on Heroku as one. Of course, you can two deployment servers where one is serving the REST API such as our backend and the other serves the client end, the build folder we just created.

Open routes/index.js and add the following line.

1// routes/index.js
2const router = require('express').Router()
3const bookRoutes = require('./books')
4const path = require('path')
5
6// API routes
7router.use('/api/books', bookRoutes)
8
9// If no API routes are hit, send the React app
10router.use(function(req, res) {
11 res.sendFile(path.join(__dirname, '../client/build/index.html'))
12})
13
14module.exports = router

Next, open the server.js to in which we add another line using Express built-in middleware that serves static assets.

1// server.js
2const express = require('express')
3const app = express()
4
5const routes = require('./routes')
6
7const PORT = process.env.PORT || 5000
8
9// require db connection
10require('./models')
11
12// configure body parser for AJAX requests
13app.use(express.urlencoded({ extended: true }))
14app.use(express.json())
15
16// ADD THIS LINE
17app.use(express.static('client/build'))
18
19app.use(routes)
20
21// Bootstrap server
22app.listen(PORT, () => {
23 console.log(`Server listening on port ${PORT}.`)
24})

Now you can open your terminal and run the following command.

1npm run start

This will trigger our server at url http://localhost:5000. Visit it using a browser and see your MERN stack app in action like below. For brevity, I haven't much styled but go ahead and showcase your CSS skills.

To verify that the data from our application is being added to the database, go to your mlab MongoDB deployment. You will notice a collection appearing with the name of books. Open it and you can see the data you have just submitted through the form. Here is how mine looks like.

I already have two records.

Since everything is running locally without any problem, we can move to the next part.

Deploying on Heroku

This is our final topic in this tutorial. Now, all you need is to have a free Heroku account and Heroku toolbelt to run the whole deployment process from your terminal.

The Heroku Command Line Interface (CLI) makes it easy to create and manage your Heroku apps directly from the terminal. It’s an essential part of using Heroku. ~ Official Documentation

To download the Heroku CLI interface visit this link. Depending on your operating system, you can download the packager. You can also choose a simpler method that is to install the cli interface using npm.

npm install -g heroku

After you go through the download and installation process, you can verify that everything has installed correctly.

1heroku --version
2# Output
3heroku/7.16.0 darwin-x64 node-v8.12.0

Modify package.json by adding the following script.

1"scripts": {
2 "start": "node server.js",
3 [...]
4 }

Login to your Heroku account with credentials by running command heroku login like below.

Next, create a Procfile in the root folder with following value.

1web: npm run start

Once you are logged in traverse to the project directory of your MERN stack application. Run the following command to create a Heroku instance of your app. Make sure you are in the main directory and not in the client folder.

Before we go on to prepare our project for Heroku, I am going to use git to push our current project. This is the most common and safe way to use it with Heroku cli interface. Run the following commands in the order described.

1# initialize our project as git repository
2git init
3
4# prepare the stage
5git add .
6
7# Commit all changes to git
8git commit -m "commit all changes"

Then run:

1heroku create

When this command runs successfully, it gives you an app id like this. Remember this app id as we are going to use it set our existing mlab MongoDB URI.

Next step is to connect the existing mlab deployment from our Heroku app.

1heroku config:set MONGODB_URI=mongodb://user:password@ds125453.mlab.com:25453/mern-example -a damp-dusk-80048

You can also use the free tier of mlab provided by Heroku using the following command in case you haven’t deployed your database previously. This command must only be run in case you are not already using mlab for your Mongodb deployment.

1heroku addons:create mongolab

You can verify that the configuration value has been set by running:

1heroku config:get MONGODB_URI --app damp-dusk-80048
2
3# Output
4
5mongodb://user:password@ds125453.mlab.com:25453/mern-example

Note that user and password in above commands are your mlab credentials that have been discussed on how to set up them in previous sections of this article. Next step is to push the whole app to Heroku.

1# Push to heroku
2
3git push heroku master

This points to Heroku remote instead of origin. This above command sends the source code to Heroku and from then Heroku reads package.json only to install dependencies. That is the reason we defined start script rather using the than server one because Heroku or a deployment server has no use of development environment dependencies such as nodemon.

Finishing the building of your project may look similar to mine.

You can then visit the URL given by Heroku like below. Do notice the already existing data that we deployed using local server in the previous section.

Conclusion

There are many deployment techniques that you can try on Heroku. The technique described in this article is just one of them. I hope you have this article has helped you out.

Originally published at Crowdbotics

I'm Aman working as an independent fullstack developer with technologies such as Node.js, ReactJS, and React Native. I try to document and write tutorials to help JavaScript, Web and Mobile developers.