Getting Started with the MERN Stack
The MERN stack consists of MongoDB, Express, React / Redux, and Node.js. Given the popularity of
React on the frontend and of Node.js on the backend, the MERN stack is one of the most popular stack
of technologies for building a modern single-page application.
In this post, we will build a todo application that utilizes a RESTful API which we will also build in the course of
this tutorial. I assume you have little knowledge about javascript(es6), reactjs and nodejs. Since we will be
using node and npm, you need to install them to your computer, you should also have your favorite code editor
already installed.
Table content
Node Server Setup
Routes
Models
Database
Testing API
Creating the Frontend
Running the React App
Creating your Components
Conclusion
Let’s start with the setup. Open your terminal and create a new file directory in any convienient location in your
local machine, and you can name it anything.
mkdir Todo
Enter into that file directory
cd Todo
At this stage we need to initialize our project with a package.json file which will contain some information about
our app and the dependencies which our app needs to run. You can use npm init or yarn init and follow the
instructions or you can use npm init -y to use the default values.
Don’t worry about the content of my package.json file, because i already installed some dependencies before
this post. As we progress into the post, yours will probably end up looking like mine.
Node Server Setup
To run our javascript code on the backend we need to spin up a server which will compile our code. We can
create our server in two ways, first is to use the built in http module in node or to make use of express.
For this project we will use express, which is a nodejs framework that handles a lot of things out of the box for
us and makes creation of RESTful API a bit simpler. To make use of express, we need to install it using yarn
or npm.
yarn add express or npm install express
Create a file index.js and type the code below into it and save
const express = require('express');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use((req, res, next) => {
res.send('Welcome to Express');
});
app.listen(port, () => {
console.log(`Server running on port ${port}`)
});
The code below helps us handle CORS related issues we might face if we try to access our API from a different
domain.
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
It’s time to start our server to see if it works. Open your terminal in the same directory as your index.js file and
type
node index.js
If everything goes well, you should see Server running on port 5000 in your terminal.
Routes
We want to achieve three things with our todo app, which is to create a task, view all task and to delete a
completed task. For this, we need to create routes which will define various endpoints that the todo app will
depend on. So let’s create a folder routes and create a file api.*_js *_with the following code in it.
const express = require ('express');
const router = express.Router();
router.get('/todos', (req, res, next) => {
});
router.post('/todos', (req, res, next) => {
});
router.delete('/todos/:id', (req, res, next) => {
})
module.exports = router;
Models
Now comes the interesting part, since our app is going to make use of mongodb which is a noSQL database, we
need to create a model and a schema. Models are defined using the Schema interface. The Schema allows you
to define the fields stored in each document along with their validation requirements and default values. In
essence the Schema is a blueprint of how the database will be constructed. In addition, you can define static
and instance helper methods to make it easier to work with your data types, and also virtual properties that you
can use like any other field, but which aren’t actually stored in the database.
To create a Schema and a model we need to install mongoose which is a node package that makes working
with mongodb easier.
yarn add mongoose or npm install mongoose
Create a new folder in your root directory and name it models, inside it create a file and name it todo.js with the
following code in it.
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
//create schema for todo
const TodoSchema = new Schema({
action: {
type: String,
required: [true, 'The todo text field is required']
}
})
//create model for todo
const Todo = mongoose.model('todo', TodoSchema);
module.exports = Todo;
Having created our model we need to update our routes to make use of the new model.
const express = require ('express');
const router = express.Router();
const Todo = require('../models/todo');
router.get('/todos', (req, res, next) => {
//this will return all the data, exposing only the id and action field to the client
Todo.find({}, 'action')
.then(data => res.json(data))
.catch(next)
});
router.post('/todos', (req, res, next) => {
if(req.body.action){
Todo.create(req.body)
.then(data => res.json(data))
.catch(next)
}else {
res.json({
error: "The input field is empty"
})
}
});
router.delete('/todos/:id', (req, res, next) => {
Todo.findOneAndDelete({"_id": req.params.id})
.then(data => res.json(data))
.catch(next)
})
module.exports = router;
Database
We need a database where we will store our data. For this we will make use of mlab. Follow this doc to get
started with mlab. After setting up your database you need to update index.js file with the following code
const express = require('express');
const bodyParser = require('body-parser');
const mongoose = require('mongoose');
const routes = require('./routes/api');
const path = require('path');
require('dotenv').config();
const app = express();
const port = process.env.PORT || 5000;
//connect to the database
mongoose.connect(process.env.DB, { useNewUrlParser: true })
.then(() => console.log(`Database connected successfully`))
.catch(err => console.log(err));
//since mongoose promise is depreciated, we overide it with node's promise
mongoose.Promise = global.Promise;
app.use((req, res, next) => {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
});
app.use(bodyParser.json());
app.use('/api', routes);
app.use((err, req, res, next) => {
console.log(err);
next();
});
app.listen(port, () => {
console.log(`Server running on port ${port}`)
});
In the code above we made use of process.env to access the environment variable which we will create now.
Create a file in your root directory with name *_.env *_and write
DB = ‘mongodb://:@ds039950.mlab.com:39950/todo’
Make sure you use your own mongodb url gotten from mlab after creating your database and user.
Replace <USER> with the username and <PASSWORD> with the password of the user you created. To work
with environment variable we have to install a node package called dotenv which makes sure we have access
to environment variable stored in the .env file.
yarn add dotenv or npm install dotenv
Then we require and configure it in our index.js file
require('dotenv').config()
The reason why I choose to adopt using environment variable in our project is that we can hide any sensitive
information from our versioning system, so it’s more of security reasons.
Testing Api
This is the part we start trying out things to make sure our RESTful API is working. Since our frontend is not
ready yet, we can make use of some API development clients to test our code. I recommend you make use of
Postman or Insomnia, if there’s any other client you have, you can still make use of it. Start your server
using node index.js then open your client, create a get method and navigate to http://localhost:5000/api/todos
You should test all the API endpoints and make sure they are working. For the endpoints that require body,
you should send json back with the necessary fields since it’s what we setup in our code.
Creating the Frontend
Since we are done with the functionality we want from our API, it’s time to create an interface for the client to
interact with the API. To start out with the frontend of the todo app, we will use create react app to scaffold our
app, so we won’t have to worry ourselves with setting up webpack and babel from scratch. In the same root
directory as your backend code, which is the todo directory, run create-react-app client or npx create-
react-app client if you don’t want to install create-react-app globally on your computer. This will create a new
folder in your root directory called client, where we will be writing all our react code.
Running the React App
After creating the react app, the next thing is to see if the app is well connected and working as well. But before
we test it, we need to perform some actions first.
Install concurrently as a dev dependency, using yarn add concurrently --dev or npm install
concurrently --save-dev. The advantage of concurrently is that it allows us to run more than one
command simultaneously from the same terminal window.
Install nodemon as a dev dependency using yarn add nodemon --dev or npm install nodemon
--save-dev . We will use nodemon to spin up our server and monitor it as well, so that if there is any
change in our server, nodemon will restart it automatically for us.
Open your package.json file in the root folder of the mern app project, and paste the following code
{
"name": "todo",
"version": "1.0.0",
"description": "building todo app using mongodb, express, react and nodejs",
"main": "index.js",
"scripts": {
"start": "node index.js",
"start-watch": "nodemon index.js",
"dev": "concurrently \"yarn run start-watch\" \"cd client && yarn start\""
},
"author": "Ojini Jude",
"license": "MIT",
"dependencies": {
"body-parser": "^1.18.3",
"dotenv": "^6.1.0",
"express": "^4.16.4",
"mongoose": "^5.3.6"
},
"devDependencies": {
"concurrently": "^4.0.1",
"nodemon": "^1.18.4"
}
}
Enter into the client folder, then locate the package.json file and add the key value pair below inside it.
This proxy setup in our package.json file will enable us make api calls without having to type the full url,
just /api/todos will get all our todos
"proxy": "http://localhost:5000"
Open your terminal and run npm run dev and make sure you are in the todo directory and not in the client
directory. Voila!, your app should be open and running on localhost:3000.
Creating your Components
One of the beautiful things about react is that it makes use of components, which are reusable and also makes
ones code modular. For our todo app, we will create two state components and one stateless component. Inside
your src folder create another folder called components and inside it create three
files Input.js, ListTodo.js and Todo.js.
Open Input.js file and paste the following
import React, { Component } from 'react';
import axios from 'axios';
class Input extends Component {
state = {
action: ""
}
addTodo = () => {
const task = {action: this.state.action}
if(task.action && task.action.length > 0){
axios.post('/api/todos', task)
.then(res => {
if(res.data){
this.props.getTodos();
this.setState({action: ""})
}
})
.catch(err => console.log(err))
}else {
console.log('input field required')
}
}
handleChange = (e) => {
this.setState({
action: e.target.value
})
}
render() {
let { action } = this.state;
return (
<div>
<input type="text" onChange={this.handleChange} value={action} />
<button onClick={this.addTodo}>add todo</button>
</div>
)
}
}
export default Input
To make use of axios, which is a Promise based HTTP client for the browser and node.js, you need to cd into
your client from your terminal and run yarn add axios or npm install axios
After that open your ListTodo.js file and paste the following code
import React from 'react';
const ListTodo = ({ todos, deleteTodo }) => {
return (
<ul>
{
todos &&
todos.length > 0 ?
(
todos.map(todo => {
return (
<li key={todo._id} onClick={() => deleteTodo(todo._id)}>{todo.action}</li>
)
})
)
:
(
<li>No todo(s) left</li>
)
}
</ul>
)
}
export default ListTodo
Then in your Todo.js file you write the following code
import React, {Component} from 'react';
import axios from 'axios';
import Input from './Input';
import ListTodo from './ListTodo';
class Todo extends Component {
state = {
todos: []
}
componentDidMount(){
this.getTodos();
}
getTodos = () => {
axios.get('/api/todos')
.then(res => {
if(res.data){
this.setState({
todos: res.data
})
}
})
.catch(err => console.log(err))
}
deleteTodo = (id) => {
axios.delete(`/api/todos/${id}`)
.then(res => {
if(res.data){
this.getTodos()
}
})
.catch(err => console.log(err))
}
render() {
let { todos } = this.state;
return(
<div>
<h1>My Todo(s)</h1>
<Input getTodos={this.getTodos}/>
<ListTodo todos={todos} deleteTodo={this.deleteTodo}/>
</div>
)
}
}
export default Todo;
We need to make little adjustment to our react code, we delete the logo and adjust our App.js to look like this.
import React from 'react';
import Todo from './components/Todo';
import './App.css';
const App = () => {
return (
<div className="App">
<Todo />
</div>
);
}
export default App;
Then we paste the following code into our App.css file.
.App {
text-align: center;
font-size: calc(10px + 2vmin);
width: 60%;
margin-left: auto;
margin-right: auto;
}
input {
height: 40px;
width: 50%;
border: none;
border-bottom: 2px #101113 solid;
background: none;
font-size: 1.5rem;
color: #787a80;
}
input:focus {
outline: none;
}
button {
width: 25%;
height: 45px;
border: none;
margin-left: 10px;
font-size: 25px;
background: #101113;
border-radius: 5px;
color: #787a80;
cursor: pointer;
}
button:focus {
outline: none;
}
ul {
list-style: none;
text-align: left;
padding: 15px;
background: #171a1f;
border-radius: 5px;
}
li {
padding: 15px;
font-size: 1.5rem;
margin-bottom: 15px;
background: #282c34;
border-radius: 5px;
overflow-wrap: break-word;
cursor: pointer;
}
@media only screen and (min-width: 300px) {
.App {
width: 80%;
}
input {
width: 100%
}
button {
width: 100%;
margin-top: 15px;
margin-left: 0;
}
}
@media only screen and (min-width: 640px) {
.App {
width: 60%;
}
input {
width: 50%;
}
button {
width: 30%;
margin-left: 10px;
margin-top: 0;
}
}
Also in our index.css file we write the following styling to it.
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
background-color: #282c34;
color: #787a80;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
Assuming we encounter no errors when we saved our code, we should have a working todo app. That has the
basic functionality we discussed earlier, which is creating a task, deleting a task and viewing all your task.
Conclusion
A journey of a thousand miles, begins with a step. I believe you’ve taken that bold step towards learning and
understanding the MERN stack. Meanwhile i have a little task for you guys. if you’ve noticed, our todo app
doesn’t have editing functionality, so i want you guys to do two things; first is to write the API code to implement
updating a previous data in the database and secondly is to add two buttons on each task, edit and delete
button, so that when as a user, I click the edit button, a popup shows, where I edit that task and save. When you
are done feel free to share your github link in the comment, also if you face any issue let’s discuss it as well.