In this tutorial, we'll create our own note-taking Notepad app using NodeJS, MongoDB, and Express. Our app will be available to create notes for now. We'll add an update, delete, and authentication feature in the future. You can check the working version of the app here. I hope you'll enjoy building this app along with me.
So, what do we need to create it?
-
A little knowledge about REST API
-
A little knowledge about Express
-
Little knowledge about MongoDB Atlas can be helpful
-
And a mandatory cup of coffee!
Introduction to Our App
Our app is a simple note-taking app where the user can input the title and description of the note, and it'll be saved in the server. Our index page will fetch all the records on the server and show it on our homepage.
Our file structure will look something like this,
|----models
|--------notes.js
|----routes
|--------routes.js
|----node_modules
|----views
|--------index.ejs
|--------new.ejs
|----.env
|----.gitignore
|----app.js
|----packge.json
|----package-lock.json
First Steps: Creating Our Server and Installing Required Packages
In NodeJS, we can create a server using the inbuilt NodeJS HTTP
module. But using this will require a lot more codes to create a simple application. We will be using a NodeJS framework called express
, which is built on top of the NodeJS HTTP
module. We'll use the app.js
file to create our server. We'll also need the mongoose
package.
npm i express mongoose
We are done installing the express and mongoose package. Now, we have to require the packages in our application. We'll do this using,
const express = require('express');
const mongoose = require('mongoose');
To initialize express, we'll use the code below,
const app = express ();
We'll be creating our server now. To create the server, we'll use the listen
method, which takes two parameters. The first parameter is the PORT to use, and the second parameter is the callback.
app.listen(process.env.PORT || 3000, () => {
console.log(`Server Has Started`);
});
If our server starts successfully, we'll get a message "Server has started" in our console.
Let's first create a basic route to see if our code is working.
app.get('/', (req, res) => {
res.send(`Yayyy! It's working`);
});
Now, if we start our server with node app.js
, we'll see that the webpage localhost:3000
returns Yayyy! It's working. Which means, we've successfully created our server. We can also initialize the app with nodemon
, but we'll discuss nodemon
in some other article.
Let's Create a MongoDB Database with MongoDB Atlas
We'll be using MongoDB Atlas to host our database. MongoDB gives a free 500mb plan, which is more than enough for our app right now. To create a database in MongoDB atlas, First, visit https://www.mongodb.com/cloud/atlas and create a new account. After creating the account, we have to create a new cluster by clicking the button.
Then click on Build a Cluster and choose the Shared Cluster from the next menu.
Choose any of the regions, change the cluster name if you want and click on Create Cluster. It'll take some time to initialize.
After the sandbox creation process is complete, click on Connect. I've allowed access from anywhere. Then Create a New Database User and note down the user ID and password. We'll need this in our app.
After creating the DB user, choose the Connection Method of Connect Your Application from the options. Copy the link provided here. We have to connect our app to this URL.
Connecting Our App with MongoDB, the URL Encoder, and the Dotenv!
We've already installed mongoose
. The mongoose client is used to connect a NodeJS application with MongoDB.
We'll also need another package now for security purposes. I think you already know about the dotenv
package. Dotenv is a zero-dependency module that loads environment variables from a .env
file into process.env
. So, we'll first install the package using npm i dotenv
. To initialize dotenv,
we'll use this line, require('dotenv').config();
in our app.js file. Now, create a new file and name it .env
. We'll put all our environment variables here.
Now add your server configuration copied from the MongoDB Atlas and create a new variable in the .env
file and initialize it with the value. Mine looks like this,
SERVER=mongodb+srv://nemo:<password>@cluster0.te7wv.gfp.mongodb.net/<dbname>?retryWrites=true&w=majority
I've created a variable and initialized it with the value. Replace the password field with the database user password I told you to note. Now, head back to the app.js
file. The mongoose.connect method is used to link the database with the app. Our app will be connected with the following code,
app.use(express.urlencoded({ extended: false }));
mongoose.connect(process.env.SERVER, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
The process.env.SERVER
refers to the variable SERVER
we created in our dotenv
file. The second parameter is used to bypass some errors. We are not going into detail what these things do.
The express .urlencoded
is a built-in middleware function available in express. It parses the incoming requests with URL-encoded payloads, and it is based on body-parser
.
This option allows us to choose between parsing the URL-encoded data with the query string library (when false) or the qs library (when true). The extended
syntax allows for rich objects and arrays to be encoded into the URL-encoded format, allowing for a JSON-like experience with URL-encoded.
Creating Our MongoDB Schema
MongoDB requires a Schema. A schema is a JSON
object that allows us to define the shape and content of documents. To create a schema, first, we'll create a new folder called models. Because we are creating a schema for our notes object, we'll create a new file called note.js inside the models directory.
Then we have to import the mongoose package inside the note.js file using const mongoose = require('mongoose');
. Our schema will look like this one,
const mongoose = require('mongoose');
const notesSchema = new mongoose.Schema({
title: {
type: String,
required: true,
},
description: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: Date.now(),
},
});
module.exports = mongoose.model('Note', notesSchema);
We have three main properties inside our schema, title
, description
, and createdAt
. I think the names define what they do. We have set a default value for the createdAt
property using the default: Date.now()
.
Finally, we're exporting the schema with the module.exports = mongoose.model('Note', notesSchema);
. The note
will be the name of the schema.
Creating the Static Pages
We'll use the ejs
template engine to render the data. To setup ejs
as a view engine, we'll set app.set('view engine', 'ejs');
in our app.js
file. Now, we can use ejs
to render web pages.
First, create a folder called views
in our root directory. The views
folder is by default the static folder in ejs
. So, we can render any static file inside this folder in the browser quickly.
We are not going to use any custom stylesheets in this article. Instead, we'll use bootstrap to style our pages.
Create a new file inside the views
folder and name it as index.ejs
. This file will work as a homepage for our app. I'm pasting the code of this page below,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Notes Tonight</title>
</head>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">???? Take Notes Tonight</h1>
<a href="/new" class="btn btn-success float-right">New Note</a>
</div>
</div>
<div class="container">
<div class="card mt-2">
<div class="card-body">
<h5 class="card-title">Article Title</h5>
<p class="text-muted">Posted On: 11/11/11</p>
<p class="card-text">The article description here</p>
</div>
</div>
</div>
</body>
</html>
As you can see, this is a pretty basic style. We are using the bootstrap jumbotron and bootstrap cards only to design. And filled up the headings with some dummy data.
Let's create another file called new.ejs
which will render the form we are going to use to create new notes. Here's the new.ejs
file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Notes Tonight</title>
</head>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">???? Take Notes Tonight</h1>
<a href="/new" class="btn btn-success float-right">New Note</a>
</div>
</div>
<div class="container">
<form action="/" method="POST">
<div class="form-group">
<label for="title">Email address</label>
<input type="text" class="form-control" name="title" placeholder="Enter Note Title" required>
</div>
<div class="form-group">
<label for="title">Description</label>
<textarea class="form-control" name="description" rows="10" required></textarea>
</div>
<div class="buttons float-right">
<a href="/" class="btn btn-secondary">Cancel</a>
<button type="submit" class="btn btn-primary">Save</button>
</div>
</form>
</div>
</body>
</html>
I don't think I've to explain it. This will render like the image below
So, our static files are ready. Now, it's time to set up our routes.
Setting Up the Routes
We'll first create a RESTful routes table to help us understand the various routes and how to use them. This table is based on Colt Steele's RESTful routes Pen on Codepen.
RESTful Routes
Name |
Path |
HTTP Method |
Purpose |
Mongoose Method |
Index |
/ |
GET |
List all the Notes |
Note.find() |
New |
/new |
GET |
Show new note form |
N/A |
Create |
/ |
POST |
Create a new note and redirect to / |
Note.create() |
By looking at this table, we can easily get an idea about how and which methods we will need for our app. We are not setting up any update or delete route in this tutorial.
Let's start with the top. We'll implement the index route in our app.js
file, and all other routes will be in a separate file.
The Index Route
As we can see from the table above, we'll need a GET
method to list all the notes. Our index route will look like this,
app.get('/', async (req, res) => {
const notes = await Note.find().sort('-createdAt');
res.render('index', { notes: notes });
});
The mongoose find()
method is an asynchronous method. For this, we are handling it with async-await
. If you are not familiar with async-await
, I'd recommend you to check this article on MDN. We are finding the notes using note .find()
and sorting the notes in descending order with the .sort('-createdAt')
method. Also, we are passing the notes that are available into a key we created in the { notes: notes }
object.
All the Other Routes
We'll put all the other routes in a separate file. First of all, create a new folder called routes
in the root directory. And create a JavaScript file inside it. I am naming it notes.js
because all my notes routes will reside here.
We usually do so because if we start putting all the routes in the app.js file itself, our app.js file will grow quite large. And it's always a good practice to keep the app.js file small and clean.
To use routes from another file, we need an express method called Router
. The router
is a small subset of the express framework. To use to Router
method, first, we need to initialize express inside our routes/notes.js
file. And then we have to call the Router method. We'll be storing the method inside a variable for our ease.
We also have to load our mongoose model because our post route will need the schema.
const express = require('express');
const router = express.Router();
const Note = require('../models/note');
router.get('/new', (req, res) => {
res.render('new');
});
Now we have to prefix all of our HTTP
methods with the router
variable. Here, we are rendering the new.ejs
file we created before.
Now, let's see how the post method will look like.
router.post('/', async (req, res) => {
let note = await new Note({
title: req.body.title,
description: req.body.description,
});
try {
note = await note.save();
res.redirect('/');
} catch (e) {
console.log(e);
res.render('new');
}
});
The requests are asynchronous in nature. We are handling those using async-await
. First, we are creating an object called note from the Note schema we defined in our mongoose model, then we are passing the values into the keys using req.body. Remember, the req.body works because of the express .urlencoded. Then, we encapsulate the save
query into a try-catch
statement for error handling and redirecting the user on the homepage if the request succeeds. Otherwise, we are console logging the error for now.
Finally, we have to export the file using module.exports = router;
. So, our complete notes.js
file inside the routes folder will look like this,
const express = require('express');
const router = express.Router();
const Note = require('../models/note');
router.get('/new', (req, res) => {
res.render('new');
});
router.post('/', async (req, res) => {
let note = await new Note({
title: req.body.title,
description: req.body.description,
});
try {
note = await note.save();
res.redirect('/');
} catch (e) {
console.log(e);
res.render('new');
}
});
module.exports = router;
Now, we'll require the models and routes inside the app.js
file. So, our complete app.js
file will be,
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const Note = require('./models/note');
const notesRouter = require('./routes/notes');
require('dotenv').config();
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: false }));
app.use(methodOverride('_method'));
app.get('/', async (req, res) => {
const notes = await Note.find().sort('-createdAt');
res.render('index', { notes: notes });
});
mongoose.connect('mongodb://localhost/notes', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
app.use('/', notesRouter);
app.listen(process.env.PORT || 3000, () => {
console.log(`Server Has Started`);
});
We're using the notes router with the following app.use('/', notesRouter);
Showing the Notes in Index Page
Now, we'll use ejs
to show our data on the homepage. Previously we've added the styles in it. It is the time to loop all our data in the index.ejs
file.
In the index.ejs
file, we'll be adding a forEach
loop to loop through the data. Because we want the card class to repeat, we'll add the loop just above the card component, inside the container.
So, our final index page will look like this,
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Notes Tonight</title>
</head>
<body>
<div class="jumbotron jumbotron-fluid">
<div class="container">
<h1 class="display-4">???? Take Notes Tonight</h1>
<a href="/new" class="btn btn-success float-right">New Note</a>
</div>
</div>
<div class="container">
<% notes.forEach(note => { %>
<div class="card mt-2">
<div class="card-body">
<h5 class="card-title"><%= note.title %></h5>
<p class="text-muted">Posted On: <%= note.createdAt.toLocaleDateString() %></p>
</div>
</div>
<%}) %>
</div>
</body>
</html>
The notes we are passing in the loop comes from the { notes: notes }
object given in the app.js
file. We are referring single notes values as note
and access the values using the dot notation like note.title
. We have also converted the ISO date using the toLocaleDateString
method available in JavaScript.
That's it. We have successfully built our NodeJS note-taking app. You can check the live version of the app here. And the whole source code is available here.
Conclusion
I hope you enjoyed building the app and also learned some new stuff. This article covered some basics of NodeJS like routing, using template engines, RESTful approach of building APIs. This can also be a quick refresher for those who already know the basics.
P.S. My version has a delete feature. We'll learn how to implement it in some other article. Until then, keep coding and stay safe.
You may also like: