We’ll be creating a minimal full-stack login/authorization app using the MERN stack (MongoDB for our database, Express and Node for our backend, and React for our frontend). We’ll also integrate Redux for state management for our React components.
Full codebase can be viewed here:
Our app will allow users to
Register
Log in
Access protected pages only accessible to logged in users
Stay logged in when they close the app or refresh the page
Log out
This should be a solid base to build off for a more functional full-stack MERN app.
This series should also enable you to more effectively build out full-stack apps using any backend / frontend.
In this part (part 1), we will
Initialize our backend using
npm
and install necessary packages
Set up a MongoDB database using
cloud mongodbSet up a server with
Node.js
and
Express
Create a
database
schema to define a User for registration and login purposes
Set up two API routes, register and login, using
passport + jsonwebtokens
for authentication and
validator
for input validation
Test our API routes using
Postman
We’ll build our backend from scratch without boilerplate code, which I feel is more ideal for first learning about MERN apps.
Before we get started
Prerequisites:
You should have at least a basic understanding of fundamental programming concepts and some experience with introductory
HTML/CSS/Javascript
. If you don’t have experience with Javascript but have worked in Python, Ruby or another similar server-side language, you should still be able to follow along.
This post is not meant to explain the
MERN stack
or the technologies in it, but is a good introduction to building a full-stack app with it. However, you can (and should) read more about the technologies included in the stack before getting started (
Mongo, Express, React, Node
).
Install
Lastly, make sure you have the following installed.
Text Editor (Atom)
(or VS code/Sublime Text)
Latest version of
Node.js
(we’ll use npm, or “Node Package Manager”, to install dependencies—much like pip for Python or gems for Ruby)
MongoDB
(quick install: install
Homebrew
and run brew update && brew install mongodb)
Postman
(for API testing)
Prettier
(to seamlessly format our Javascript; in Atom,
Packages → Prettier → Toggle Format on Save
to automatically format on save)
Let’s get started.
Part 1: Creating our backend
i. Initializing our project
Set the current directory to wherever you want your project to live and initialize the project using npm.
➜ ~ mkdir mern-auth
➜ ~ cd mern-auth
➜ mern-auth npm init
After running the command, a utility will walk you through creating a
package.json
file.
You can enter through most of these safely, but go ahead and set the entry point to
server.js
instead of the default
index.js
when prompted (can do this later in our package.json).
ii. Setting up our package.json
1. Set the “main”
entry point to “
server.js
” instead of the default “
index.js
”, if you haven’t done so already (for conventional purposes)
2. Install the following dependencies using npm
➜
mern-auth
npm i bcryptjs body-parser concurrently express is-empty jsonwebtoken mongoose passport passport-jwt validator
A brief description of each package and the function it will serve:
bcryptjs
: used to hash passwords before we store them in our database
body-parser
: used to parse incoming request bodies in a middleware
concurrently
: allows us to run our backend and frontend concurrently and on different ports
express
: sits on top of Node to make the routing, request handling, and responding easier to write
is-empty
: global function that will come in handy when we use validator
jsonwebtoken
: used for authorization
mongoose
: used to interact with MongoDB
passport
: used to authenticate requests, which it does through an extensible set of plugins known as strategies
passport-jwt
: passport strategy for authenticating with a JSON Web Token (JWT); lets you authenticate endpoints using a JWT
validator
: used to validate inputs (e.g. check for valid email format, confirming passwords match)
3. Install the following devDependency (-D) using npm
➜ mern-auth npm i -D nodemon
Nodemon is a utility that will monitor for any changes in your code and automatically restart your server, which is perfect for development. The alternative would be having to take down your server (Ctrl+C) and stand it back up every time you made a change. Not ideal.
Make sure to use nodemon instead of node when you run your code for development purposes.
4. Change the
“scripts”
object to the following
"scripts": {
"start": "node server.js",
"server": "nodemon server.js",
},
Later on, we’ll use nodemon run server
to run our dev server.
Your
package.json
file should look like the following at this stage.
{
"name": "mern_auth",
"version": "1.0.0",
"description": "mern auth",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Patrick Biyaga",
"license": "ISC",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.19.0",
"concurrently": "^5.3.0",
"express": "^4.17.1",
"is-empty": "^1.2.0",
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.11.8",
"passport": "^0.4.1",
"passport-jwt": "^4.0.0",
"validator": "^13.5.2"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}
iii. Setting up our database
1. Head over to
cloud mongodb
and create an account if you don’t have one already
2. Create a new MongoDB Deployment
Select
AWS
as your cloud provider and
Sandbox
as your plan type. Then set your AWS region based on where you live. Finally, name your database and submit your order (don’t worry, it’s free).
3. Head over to your dashboard and click on your newly created database
Navigate to the Users tab, click Add Database User, and create a database user. Your database needs at least one user in order to use it.
Find your
MongoDB URI
; we will use this to connect to our database.
mongodb://<dbuser>:<dbpassword>@ds159993.mlab.com:59993/mern-auth
Replace <dbuser> and <dbpassword> with the database user credentials you just created.
4. Create a config
directory and within it a keys.js
file
➜ mern-auth mkdir config && cd config && touch keys.js
Within your keys.js file, let’s place the following for easy access outside of this file.
module.exports = { mongoURI: "YOUR_MONGOURI_HERE" };
And that’s it for this file, for now.
iv. Setting up our server with Node.js and Express
The basic flow for our server setup is as follows.
Pull in our required dependencies (namely express, mongoose and bodyParser)
Initialize our app using express()
Apply the middleware function for bodyparser so we can use it
Pull in our MongoURI from our keys.js file and connect to our MongoDB database
Set the port for our server to run on and have our app listen on this port
Let’s place the following in our server.js file.
const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const app = express();
// Bodyparser middleware
app.use(
bodyParser.urlencoded({
extended: false
})
);
app.use(bodyParser.json());
// DB Config
const db = require("./config/keys").mongoURI;
// Connect to MongoDB
mongoose
.connect(
db,
{ useNewUrlParser: true }
)
.then(() => console.log("MongoDB successfully connected"))
.catch(err => console.log(err));
const port = process.env.PORT || 5000; // process.env.port is Heroku's port if you choose to deploy the app there
app.listen(port, () => console.log(`Server up and running on port ${port} !`));
Run
nodemon run server
and the following should output.
➜ mern-auth nodemon run server
[nodemon] 1.18.3
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node run server server.js`
Server up and running on port 5000 !
MongoDB successfully connected
Try changing the "Server up and running..." message in your file, hit save and you should see your server automatically restart.
Congratulations! You’ve set up a server using NodeJS and Express and successfully connected to your MongoDB database.
v. Setting up our database schema
Let’s create a models folder to define our user schema. Within models, create a User.js file.
➜ mern-auth mkdir models && cd models && touch User.js
Within User.js, we will
Pull in our required dependencies
Create a Schema to represent a User, defining fields and types as objects of the Schema
Export the model so we can access it outside of this file
Let’s place the following in our User.js
file.
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
// Create Schema
const UserSchema = new Schema({
name: {
type: String,
required: true
},
email: {
type: String,
required: true
},
password: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
}
});
module.exports = User = mongoose.model("users", UserSchema);
Pretty standard set up for what you would expect a user to have.
vi. Setting up form validation
Before we set up our routes, let’s create a directory for input validation and create a register.js and login.js file for each route’s validation.
➜ mern-auth mkdir validation && cd validation && touch register.js login.js
Our validation flow for our register.js file will go as follows:
Pull in validator and is-empty dependencies
Export the function validateRegisterInput, which takes in data as a parameter (sent from our frontend registration form, which we’ll build in Part 2)
Instantiate our errors object
Convert all empty fields to an empty string before running validation checks (validator only works with strings)
Check for empty fields, valid email formats, password requirements and confirm password equality using validator functions
Return our errors object with any and all errors contained as well as an isValid boolean that checks to see if we have any errors
Let’s place the following in register.js.
const Validator = require("validator");
const isEmpty = require("is-empty");
module.exports = function validateRegisterInput(data) {
let errors = {};
// Convert empty fields to an empty string so we can use validator functions
data.name = !isEmpty(data.name) ? data.name : "";
data.email = !isEmpty(data.email) ? data.email : "";
data.password = !isEmpty(data.password) ? data.password : "";
data.password2 = !isEmpty(data.password2) ? data.password2 : "";
// Name checks
if (Validator.isEmpty(data.name)) {
errors.name = "Name field is required";
}
// Email checks
if (Validator.isEmpty(data.email)) {
errors.email = "Email field is required";
} else if (!Validator.isEmail(data.email)) {
errors.email = "Email is invalid";
}
// Password checks
if (Validator.isEmpty(data.password)) {
errors.password = "Password field is required";
}
if (Validator.isEmpty(data.password2)) {
errors.password2 = "Confirm password field is required";
}
if (!Validator.isLength(data.password, { min: 6, max: 30 })) {
errors.password = "Password must be at least 6 characters";
}
if (!Validator.equals(data.password, data.password2)) {
errors.password2 = "Passwords must match";
}
return {
errors,
isValid: isEmpty(errors)
};
};
Our validation for our login.js follows an identical flow to the above, albeit with different fields.
const Validator = require("validator");
const isEmpty = require("is-empty");
module.exports = function validateLoginInput(data) {
let errors = {};
// Convert empty fields to an empty string so we can use validator functions
data.email = !isEmpty(data.email) ? data.email : "";
data.password = !isEmpty(data.password) ? data.password : "";
// Email checks
if (Validator.isEmpty(data.email)) {
errors.email = "Email field is required";
} else if (!Validator.isEmail(data.email)) {
errors.email = "Email is invalid";
}
// Password checks
if (Validator.isEmpty(data.password)) {
errors.password = "Password field is required";
}
return {
errors,
isValid: isEmpty(errors)
};
};
vii. Setting up our API routes
Now that we have validation handled, let’s create a new folder for our api routes and create a users.js file for registration and login.
➜ mern-auth mkdir routes && cd routes && mkdir api && cd api && touch users.js
At the top of users.js, let’s pull in our required dependencies and load our input validations & user model.
const express = require("express");
const router = express.Router();
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const keys = require("../../config/keys");
// Load input validation
const validateRegisterInput = require("../../validation/register");
const validateLoginInput = require("../../validation/login");
// Load User model
const User = require("../../models/User");
Create the Register endpoint
For our register endpoint, we will
Pull the errors and isValid variables from our validateRegisterInput(req.body) function and check input validation
If valid input, use MongoDB’s User.findOne() to see if the user already exists
If user is a new user, fill in the fields (name, email, password) with data sent in the body of the request
Use bcryptjs to hash the password before storing it in your database
Let’s place the following in our users.js file for our register route.
// @route POST api/users/register
// @desc Register user
// @access Public
router.post("/register", (req, res) => {
// Form validation
const { errors, isValid } = validateRegisterInput(req.body);
// Check validation
if (!isValid) {
return res.status(400).json(errors);
}
User.findOne({ email: req.body.email }).then(user => {
if (user) {
return res.status(400).json({ email: "Email already exists" });
} else {
const newUser = new User({
name: req.body.name,
email: req.body.email,
password: req.body.password
});
// Hash password before saving in database
bcrypt.genSalt(10, (err, salt) => {
bcrypt.hash(newUser.password, salt, (err, hash) => {
if (err) throw err;
newUser.password = hash;
newUser
.save()
.then(user => res.json(user))
.catch(err => console.log(err));
});
});
}
});
});
Setup passport
In your config directory, create a passport.js file.
➜ mern-auth cd config && touch passport.js
Before we setup passport, let’s add the following to our keys.js file.
module.exports = {
mongoURI: "YOUR_MONGOURI_HERE",
secretOrKey: "secret"
};
Back to passport.js. You can read more about
the passport-jwt strategy
in the link below. It does a great job breaking down how the JWT authentication strategy is constructed, explaining required parameters, variables and functions such as options, secretOrKey, jwtFromRequest, verify, and jwt_payload.
Let’s place the following in our passport.js file.
const JwtStrategy = require("passport-jwt").Strategy;
const ExtractJwt = require("passport-jwt").ExtractJwt;
const mongoose = require("mongoose");
const User = mongoose.model("users");
const keys = require("../config/keys");
const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.secretOrKey;
module.exports = passport => {
passport.use(
new JwtStrategy(opts, (jwt_payload, done) => {
User.findById(jwt_payload.id)
.then(user => {
if (user) {
return done(null, user);
}
return done(null, false);
})
.catch(err => console.log(err));
})
);
};
Also, note that the jwt_payload will be sent via our login endpoint below.
Create the Login endpoint
For our login endpoint, we will
Pull the errors and isValid variables from our validateLoginInput(req.body) function and check input validation
If valid input, use MongoDB’s User.findOne() to see if the user exists
If user exists, use bcryptjs to compare submitted password with hashed password in our database
If passwords match, create our JWT Payload
Sign our jwt, including our payload, keys.secretOrKey from keys.js, and setting a expiresIn time (in seconds)
If successful, append the token to a Bearer string (remember in our passport.js file, we setopts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();)
Let’s place the following in our users.js
file for our login route.
// @route POST api/users/login
// @desc Login user and return JWT token
// @access Public
router.post("/login", (req, res) => {
// Form validation
const { errors, isValid } = validateLoginInput(req.body);
// Check validation
if (!isValid) {
return res.status(400).json(errors);
}
const email = req.body.email;
const password = req.body.password;
// Find user by email
User.findOne({ email }).then(user => {
// Check if user exists
if (!user) {
return res.status(404).json({ emailnotfound: "Email not found" });
}
// Check password
bcrypt.compare(password, user.password).then(isMatch => {
if (isMatch) {
// User matched
// Create JWT Payload
const payload = {
id: user.id,
name: user.name
};
// Sign token
jwt.sign(
payload,
keys.secretOrKey,
{
expiresIn: 31556926 // 1 year in seconds
},
(err, token) => {
res.json({
success: true,
token: "Bearer " + token
});
}
);
} else {
return res
.status(400)
.json({ passwordincorrect: "Password incorrect" });
}
});
});
});
Don’t forget to export our router at the bottom of users.js
so we can use it elsewhere.
module.exports = router;
Pulling our routes into our server.js file
Make the following
bolded
additions to server.js.
const express = require("express");
const mongoose = require("mongoose");
const bodyParser = require("body-parser");
const passport = require("passport");
const users = require("./routes/api/users");
const app = express();
// Bodyparser middleware
app.use(
bodyParser.urlencoded({
extended: false
})
);
app.use(bodyParser.json());
// DB Config
const db = require("./config/keys").mongoURI;
// Connect to MongoDB
mongoose
.connect(
db,
{ useNewUrlParser: true }
)
.then(() => console.log("MongoDB successfully connected"))
.catch(err => console.log(err));
// Passport middleware
app.use(passport.initialize());
// Passport config
require("./config/passport")(passport);
// Routes
app.use("/api/users", users);
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Server up and running on port ${port} !`));
viii. Testing our API routes using Postman
Testing our Register endpoint
Open Postman and
Set the request type to POST
Set the request url to
http://localhost:5000/api/users/register
Navigate to the Body tab, select x-www-form-urlencoded, fill in your registration parameters and hit Send
You should receive a HTTP status response of 200 OK and have the new user returned as JSON.
Check your database on cloud mongodb and you should see a new user created with the above credentials.
Testing our Login endpoint
Similar to the above, in Postman
Set the request type to POST
Set the request url to
http://localhost:5000/api/users/l
ogin
Navigate to the Body tab, select x-www-form-urlencoded, fill in your login parameters and hit Send
You should receive a HTTP status response of 200 OK and have the jwt returned in the response.
Testing errors
You should play around and test your validator errors by playing around with different cases for when a user signs up and logs in (e.g. invalid email formats, passwords that don’t match). When you test the API out in Postman, you should see your errors object returned.
We’ll eventually bring these errors into our frontend and display the messages within the form itself.
And that’s it for our backend!
We’ve successfully set up and tested our API routes (using passport and jsonwebtokens for authentication). Give yourself a pat on the back for following along. Throw some claps too. 👏
In
Part 2
(see below), we’ll create our frontend using React, leverage Redux for state management and begin to use axios to fetch data from our server.