Build a Login/Auth App with the MERN Stack — Part 1 (Backend)
Create a (minimal) full-stack app with user authentication via passport and JWTs.
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. Check out the following for examples of projects I built using this mern-auth base.
- Feeder (Github Repo and Medium tutorial): npm package to collect feedback from users through an embeddable widget/component and admin dashboard to manage projects/view feedback
- Teams (Github Repo): MERN dashboard web app and Asana clone
- Banking Web app with Plaid (Medium tutorial): Medium tutorial on building a personal finance web app using Plaid and the MERN Stack
This series should also enable you to more effectively build out full-stack apps using any backend / frontend. For example, this project taught me what I needed to know to build IB Vine, which I built using Gatsby (React.js framework), Redux, and Firebase for the backend / database.
In this part (part 1), we will
- Initialize our backend using
npm
and install necessary packages - Set up a MongoDB database using mLab
- Set up a server with
Node.js
andExpress
- Create a database schema to define a
User
for registration and login purposes - Set up two API routes,
register
andlogin
, usingpassport
+jsonwebtoken
s for authentication andvalidator
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.
Shared in Bit’s blog
Bit helps you share and sync Javascript and UI components in different projects and apps, to build faster with your team. It’s OSS, give it a try.
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
).
Here are two good starter resources.
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 likepip
for Python orgems
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 databasebody-parser
: used to parse incoming request bodies in a middlewareconcurrently
: allows us to run our backend and frontend concurrently and on different portsexpress
: sits on top ofNode
to make the routing, request handling, and responding easier to writeis-empty
: global function that will come in handy when we usevalidator
jsonwebtoken
: used for authorizationmongoose
: used to interact with MongoDBpassport
: used to authenticate requests, which it does through an extensible set of plugins known asstrategies
passport-jwt
:passport
strategy for authenticating with a JSON Web Token (JWT); lets you authenticate endpoints using a JWTvalidator
: 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 Example",
"main": "server.js",
"scripts": {
"start": "node server.js",
"server": "nodemon server.js"
},
"author": "",
"license": "MIT",
"dependencies": {
"bcryptjs": "^2.4.3",
"body-parser": "^1.18.3",
"concurrently": "^4.0.1",
"express": "^4.16.4",
"is-empty": "^1.2.0",
"jsonwebtoken": "^8.3.0",
"mongoose": "^5.3.11",
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"validator": "^10.9.0"
}
}
iii. Setting up our database
1. Head over to mLab 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
andbodyParser
) - Initialize our app using
express()
- Apply the middleware function for
bodyparser
so we can use it - Pull in our
MongoURI
from ourkeys.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 thereapp.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
andis-empty
dependencies - Export the function
validateRegisterInput
, which takes indata
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 anisValid
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)
};
};
💡 You can now encapsulate and move the validation logic out of your app code into a separate validation module/function, and package, test, document, and publish it independently using Bit so you (or others) can import it into any project with a simple
bit import your.username/validationComponent
command.
Learn more:
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
andisValid
variables from ourvalidateRegisterInput(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 validationconst { 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
andisValid
variables from ourvalidateLoginInput(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 ourpayload
,keys.secretOrKey
fromkeys.js
, and setting aexpiresIn
time (in seconds) - If successful, append the token to a
Bearer
string (remember in ourpassport.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 validationconst { 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, selectx-www-form-urlencoded
, fill in your registration parameters and hitSend
You should receive a HTTP status response of 200 OK
and have the new user returned as JSON
.
Check your database on mLab 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/login
- Navigate to the
Body
tab, selectx-www-form-urlencoded
, fill in your login parameters and hitSend
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 jsonwebtoken
s 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.
Build Apps with reusable components, just like Lego
Bit’s open-source tool help 250,000+ devs to build apps with components.
Turn any UI, feature, or page into a reusable component — and share it across your applications. It’s easier to collaborate and build faster.
Split apps into components to make app development easier, and enjoy the best experience for the workflows you want: