The MERN stack consists of MongoDB, Express, React / Redux, and Node.js. The MERN stack is one of the most popular JavaScript stacks for building modern single-page web applications.
In this tutorial, you will build a todo application that uses a RESTful API, which you will also build later in this tutorial.
To complete this tutorial, you will need:
Note: This tutorial was originally written to use the mLab service to host a MongoDB database. mLab has been closed to new account creation since February 2019 and they suggest using MongoDB Atlas.
It is also possible to follow the installation instructions and run MongoDB locally, but this tutorial will not cover that process and is left as-is for educational purposes.
You will also need a code editor that you are familiar with, preferably one that has support for JavaScript code highlighting.
Downloading and installing a tool like Postman is recommended for testing API endpoints.
This tutorial was verified with Node v14.2.0, npm
v6.14.5, mongodb-community
v4.2.6, express
v4.17.1, and mongoose
v5.9.17.
Let’s start with the setup. Open your terminal and create a new file directory in any convenient location on your local machine. You can name it anything but in this example, it is called mern-todo
.
- mkdir mern-todo
Now, enter into that file directory:
- cd mern-todo
The next step is to initialize the project with a package.json
file. This file will contain some information about your app and the dependencies that it needs to run.
You can use:
- npm init
And follow the instructions when prompted. Or you can use:
- npm init -y
To use the default values.
To run your JavaScript code on the backend, you need to spin up a server that will compile your code.
The server can be created in two ways: first is to use the built-in http
module in Node; second is to make use of the Express.js framework.
This tutorial will use Express.js. It is a Node.js HTTP framework that handles a lot of things out of the box and requires little code to create fully functional RESTful APIs. To use Express, install it using npm:
- npm install express
Now, create a file index.js
and type the following code into it and save:
const express = require('express');
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}`);
});
This snippet from the preceeding code helps handle CORS related issues that you might face when trying to access the API from different domains during development and testing:
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 your 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 will see Server running on port 5000 in your terminal.
There are three things that the app needs to do:
For each task, you will need to create routes that will define multiple 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.
- mkdir routes
Edit api.js
and paste the following code in it:
const express = require('express');
const router = express.Router();
router.get('/todos', (req, res, next) => {
// get placeholder
});
router.post('/todos', (req, res, next) => {
// post placeholder
});
router.delete('/todos/:id', (req, res, next) => {
// delete placeholder
});
module.exports = router;
This provides placeholder routes for GET, POST, and DELETE.
Now, comes the interesting part. Since the 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 stored in the database.
To create a schema and a model, install Mongoose which is a Node package that makes working with MongoDB easier.
- # ensure that you are in the `mern-todo` project directory
- 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:
- mkdir models
Paste the following into todo.js
with your text editor:
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;
Now, 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;
You will need a database where you will store your data. For this, you will make use of mLab. Follow the documentation 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');
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's Promise is deprecated, we override 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}`);
});
Note: In versions prior to Express 4.16+ it was necessary to rely upon middleware like body-parser
. However, it is now possible to use the built-in parser.
If you are using an older version of Express, use npm to install body-parser
:
- npm install body-parser
In the preceeding code made use of process.env
to access environment variables, which need to be created. Create a file in your root directory with the name .env
and edit:
DB = 'mongodb://<USER>:<PASSWORD>@example.mlab.com:port/todo'
Make sure you use your own MongoDB URL from mLab after you created your database and user. Replace <USER>
with the username and <PASSWORD>
with the password of the user you created.
To work with environment variable you will have to install a Node package called dotenv
that makes sure you have access to environment variable stored in the .env
file.
- # ensure that you are in the `mern-todo` project directory
- npm install dotenv
Then require and configure it in index.js
:
require('dotenv').config()
Using environment variables instead of writing credentials to the application code directly can hide sensitive information from your versioning system. It is considered a best practice to separate configuration and secret data from application code in this manner.
This is the part we start trying out things to make sure your RESTful API is working. Since your frontend is not ready yet, you can make use of some API development clients to test your code.
You can use Postman or Insomnia or your preferred client for testing APIs.
Start your server using the command:
- node index.js
Now, open your client, create a GET method and navigate to http://localhost:5000/api/todos
.
Test all the API endpoints and make sure they are working. For the endpoints that require body
, send JSON back with the necessary fields since it’s what you set up in your code.
Sample POST request:
POST localhost:5000/api/todos
Body
raw
Sample POST value:
{
"action": "build a mern stack application"
}
Sample GET request:
GET localhost:5000/api/todos
Sample GET response:
Output[
{
"id": "5bd4edfc89d4c3228e1bbe0a",
"action": "build a mern stack application"
}
]
Sample DELETE request:
DELETE localhost:5000/api/todos/5bd4edfc89d4c3228e1bbe0ad
Test and observe the results of GET, POST, and DELETE.
Since you are done with the functionality you want from your API, it is time to create an interface for the client to interact with the API. To start out with the frontend of the todo app, you will use the create-react-app
command to scaffold your app.
In the same root directory as your backend code, which is the mern-todo
directory, run:
- npx create-react-app client
This will create a new folder in your mern-todo
directory called client
, where you will add all the React code.
Before testing the React app, there are many dependencies that need to be installed in the project root directory.
First, install concurrently
as a dev dependency:
- npm install concurrently --save-dev
Concurrently is used to run more than one command simultaneously from the same terminal window.
Then, install nodemon
as a dev dependency:
- npm install nodemon --save-dev
Nodemon is used to run the server and monitor it as well. If there is any change in the server code, Nodemon will restart it automatically with the new changes.
Next, open your package.json
file in the root folder of the app project, and paste the following code:
{
// ...
"scripts": {
"start": "node index.js",
"start-watch": "nodemon index.js",
"dev": "concurrently \"npm run start-watch\" \"cd client && npm start\""
},
// ...
}
Enter into the client folder, then locate the package.json
file and add the following key-value pair inside it.
{
// ...
"proxy": "http://localhost:5000"
}
This proxy setup in our package.json
file will enable you to make API calls without having to type the full URL, just /api/todos
will get all your todos
Open your terminal and run npm run dev
and make sure you are in the todo
directory and not in the client
directory.
Your app will be open and running on localhost:3000
.
One of the advantages of React is that it makes use of components, which are reusable and also makes code modular. For your todo app, there will be 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 will need to navigate to your client
directory from your terminal:
- cd client
And run npm install axios
:
- 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;
You will need to make a little adjustment to your React code. Delete the logo and adjust your 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 paste the following code into App.css
:
.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 index.css
add the following rules:
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 no errors when saving all these files, the todo app will be ready and fully functional with the functionality discussed earlier: creating a task, deleting a task, and viewing all your tasks.
In this tutorial, you created a todo app using the MERN stack. You wrote a frontend application using React that communicates with a backend application written using Express.js. You also created a MongoDB backend for storing tasks in a database.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!
Can we have as well a tutorial on how to deploy such apps on Digital Ocean ?
I am struggling to deploy MERN app to digital ocean. Specially on connecting them together. In localhost we do it with concurrently. What to do in the server? Please help!
Maybe it’s easier to fix the tutorial, wasted a lot of time on it. tesing the API, without using Windows, MacOS or Ubuntu is bad.
Next time, follow the tutorial and figure out if it works.
This comment has been deleted