Project Introduction
Chris Anderson from Microsoft once described Javascript as the "English of Programming Languages" -
a lot of people can speak at least a little bit of it, even it it's bad. It's not a perfect
language, but it's a language which is relatively easy to learn for a lot of people.
In this tutorial, I'll show you how to build a simple REST API using Node.js, Express, MongoDB, and
Mongoose.
MongoDB
For this project, I'm going to be using a cloud-hosted, free version of MongoDB called
MongoDB Atlas. Go ahead and click the link, sign up for an
account, and follow this guide. You should end up
with a connection string that looks similar to:
1mongodb+srv://<dbusername>:<password>@dev-nycjj.mongodb.net/<dbname>?retryWrites=true&w=majority
Copy that string to your clipboard and head into the next step.
Folder Structure & Initial Routes
Open up your terminal and navigate to the directory you'd like to create your project folder in.
Copy and paste the following line into your terminal
1mkdir PROJECT_NAME && cd PROJECT_NAME && npm init
Walk through each step of the npm init
script (our entry point is going to be index.js
).
When you are done, create a .env
file in the root of your project directory.
Open the .env
file in your text editor and enter:
1MONGODB_URI=URL_YOU_COPIED_FROM_EARLIER
Save that file, and head back to your terminal. Run the following:
1npm install express mongoose bcryptjs cors dotenv jsonwebtoken body-parser passport-jwt passport
Once installed, create an index.js
file in the root directory of your project to serve as
your main entry point file. You'll want to import all of the modules you need for this file, found
below:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
Below your imports, initialize your app variable:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9
Right under that, create a variable for my port number so that it is easily accessible and can be
modified from one location in your file:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10
Then go ahead and use app.listen
to tell your app which port to listen for:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11
12app.get("/", (req, res) => {
13 res.send("Hello, world!");
14});
15
16app.listen(port, () => {
17 console.log(`Server running at: http://localhost:${port}`);
18});
You should have enough code to successfully run your server to check if you've run into any bugs at
this point.
Now we want to install Nodemon globally, so that we don't need to stop and
start our node server everytime we make a change to a file.
Once installed, run nodemon
in your terminal from within your application directory and
wait for your Server running at: ...
message to let you know your app is running on the
port you specified earlier.
Next, we'll go ahead and install our CORS middleware, so that we can make requests to this API from
a different domain name. If you'd like to learn more about CORS and what it does, the MDN docs have
a great article I recommend reading.
Since we installed the CORS npm module in the beginning of this project, integrating the CORS
middleware is as simple as adding:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11
13
14app.get("/", (req, res) => {
15 res.send("Hello, world!");
16});
17
18app.listen(port, () => {
19 console.log(`Server running at: http://localhost:${port}`);
20});
The module basically does us the favor of injecting different headers within
our application
using res.header
. We'll be using this module with another module we installed called
body-parser
, which parses incoming request bodies. For example, when you receive
a form submission, body-parser will help you parse the form input data. When you
receive a GET
request with a string query, body-parser will help you parse that
URL parameter for validation.
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11
12app.use(cors());
13app.use(bodyParser.json());
14
15app.get("/", (req, res) => {
16 res.send("Hello, world!");
17});
18
19app.listen(port, () => {
20 console.log(`Server running at: http://localhost:${port}`);
21});
Next, let's make use of the Express router so that we can encapsulate all of the user routes in
another file without cluttering our main entry file. Go ahead and create a new constant called users
and have it require the file where we will store our routes. We
also need to mount another piece of middleware on the app
variable to
add the /users
prefix to all the routes in the file we create below.
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11const users = require("./routes/users");
12
13app.use(cors());
14app.use(bodyParser.json());
15app.use("/users", users);
16
17app.get("/", (req, res) => {
18 res.send("Hello, world!");
19});
20
21app.listen(port, () => {
22 console.log(`Server running at: http://localhost:${port}`);
23});
If you look at nodemon, you'll notice that your app crashed, because it couldn't find the file ./routes/users
Create a new directory in your project to match that route
we required just now, call it 'routes' and save it. Next, create a new file in that
directory called 'users.js' and require the following modules:
1const express = require("express");
2const router = express.Router();
3
4module.exports = router;
Nodemon should be back up and running.
Next, we'll create the three routes that users should have the ability to interact with; a
registration route, an authentication route, and a profile route (which we will eventually protect
via the JWT tokenization).
1const express = require("express");
2const router = express.Router();
3
4router.post("/register", (req, res, next) => {
5 res.send("REGISTER");
6});
7
8router.post("/authenticate", (req, res, next) => {
9 res.send("AUTHENTICATE");
10});
11
12router.get("/profile", (req, res, next) => {
13 res.send("PROFILE");
14});
15
16module.exports = router;
Of course, the parameters being passed into the .send methods are just placeholders, we'll go ahead
and fill those out in just a minute.
To test that everything works so far, navigate to localhost:3000/users/authenticate
, and
you should see AUTHENTICATE
on the screen.
To connect to the database, Mongoose exposes a pretty straightforward 'connect' function that you
add to your entry file which will run as soon as your start your application to open the port to
your database.
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11const users = require("./routes/users");
12
13app.use(cors());
14app.use(bodyParser.json());
15app.use("/users", users);
16
17mongoose
18 .connect(process.env.MONGODB_URI, { useNewUrlParser: true })
19 .then(() => {
20 console.log("Successfully connected to MongoDB");
21 })
22 .catch((error) => console.error(error));
23
24mongoose.connection.on("error", (err) => {
25 console.error("Connection to MongoDB interrupted, attempting to reconnect");
26});
27
28app.get("/", (req, res) => {
29 res.send("Hello, world!");
30});
31
32app.listen(port, () => {
33 console.log(`Server running at: http://localhost:${port}`);
34});
Save that file, and nodemon should have refreshed with a new console log letting you know you're
connected to the database found in your config file.
If you are curious as to why there are two types of error methods that Mongo needs, it's because
there are two classes of errors that can occur with a Mongoose connection.
- Error on initial connection. If initial connection fails, Mongoose will not attempt to reconnect,
it will emit an
error
event, and the promise mongoose.connect()
returns will
reject.
- Error after initial connection was established. Mongoose will attempt to reconnect, and it will
emit an
error
event.
Part 4 - User Model
Next we'll be creating our user model file to handle data such as name, password, email, and
username. We'll also have our functions that interact with the database in that file.
Create a directory called models
and require the following modules inside a file called
user.js
:
1const mongoose = require("mongoose");
2const bcrypt = require("bcryptjs");
Now let's create the user schema:
1const mongoose = require("mongoose");
2const bcrypt = require("bcryptjs");
3
4const UserSchema = mongoose.Schema({
5 name: {
6 type: String,
7 },
8 email: {
9 type: String,
10 required: true,
11 },
12 password: {
13 type: String,
14 required: true,
15 },
16});
17
18module.exports = mongoose.model("User", UserSchema);
Part 5 - The 'REGISTER' Path
Back in the root directory of your file navigate to routes/users.js
and require the
schema we created in part 4 to the top of the file:
1const express = require("express");
2const router = express.Router();
3
4const User = require("../models/user");
5
6router.post("/register", (req, res, next) => {
7 res.send("REGISTER");
8});
9
10router.post("/authenticate", (req, res, next) => {
11 res.send("AUTHENTICATE");
12});
13
14router.get("/profile", (req, res, next) => {
15 res.send("PROFILE");
16});
17
18module.exports = router;
Navigate down to the router.post
request for the /register
path and change the
callback function body to:
1const express = require("express");
2const router = express.Router();
3
4const User = require("../models/user");
5
6router.post("/register", (req, res, next) => {
7 /**
8 * Note, there is nothing in this code to stop someone from registering
9 * the same email twice. This is just an example application to explain
10 * high level concepts, but if I were building this to be deployed into
11 * production, I would add a function to search the database
12 * for a user with the email in the request right here.
13 */
14 let newUser = new User({
15 name: req.body.name,
16 email: req.body.email,
17 username: req.body.username,
18 password: req.body.password,
19 });
20
21 newUser.save((err) => {
22 if (err) throw new Error("User did not save");
23 console.log("User saved", newUser);
24 res.status(201).send("User created!");
25 });
26});
27
28router.post("/authenticate", (req, res, next) => {
29 res.send("AUTHENTICATE");
30});
31
32router.get("/profile", (req, res, next) => {
33 res.send("PROFILE");
34});
35
36module.exports = router;
Head back to models/user.js
and add the pre
middleware function that we're using above at the
bottom of the user.js
model file:
1const mongoose = require("mongoose");
2const bcrypt = require("bcryptjs");
3
4const UserSchema = mongoose.Schema({
5 name: {
6 type: String,
7 },
8 email: {
9 type: String,
10 required: true,
11 },
12 password: {
13 type: String,
14 required: true,
15 },
16});
17
18UserSchema.pre("save", function (next) {
19 var user = this;
20 if (!user.isModified("password")) return next();
21
22 bcrypt.genSalt(5, function (err, salt) {
23 if (err) return next(err);
24
25 bcrypt.hash(user.password, salt, function (err, hash) {
26 if (err) return next(err);
27 user.password = hash;
28 next();
29 });
30 });
31});
32
33module.exports = mongoose.model("User", UserSchema);
Part 6 - Try it Out
At this point, we can go ahead and make a POST request to our application's register
endpoint with some user data in the POST body to ensure that the application properly saves it.
I'll be using Postman, but feel free to user any other utility that is capable of sending HTTP
requests like curl
or HTTPie.
Make your POST request to http://localhost:3000/users/register
and then for your post
body, go ahead and pass a JSON object that looks like:
1{
2 "name": "John Doe",
3 "email": "jdoe@gmail.com",
4 "username": "john",
5 "password": "123456"
6}
You should receive a response body that looks like:
You can then log into your MongoDB Atlas account, find your cluster, click the button that says
"Collections" and you should be able to view the information you just saved
*Note: You can refactor the response on the /register
POST
method to keep
the user logged in after registering, because right now this endpoint just creates the user and the
user would be expected to manually log in after creating their account. I suggest waiting until
finishing the next section, Part 7 - Authentication before doing that.
Part 7 - Authentication
In this part, we will set up Passport.js with a JWT strategy to authenticate users and receive
tokens.
In our index.js
file, add the following lines of code:
1require("dotenv").config();
2const passport = require("passport");
3const express = require("express");
4const path = require("path");
5const bodyParser = require("body-parser");
6const cors = require("cors");
7const mongoose = require("mongoose");
8mongoose.set("useUnifiedTopology", true);
9const app = express();
10const port = 3000;
11const users = require("./routes/users");
12
13app.use(cors());
14app.use(bodyParser.json());
15app.use("/users", users);
16
17app.use(passport.initialize());
18app.use(passport.session());
19
20mongoose
21 .connect(process.env.MONGODB_URI, { useNewUrlParser: true })
22 .then(() => {
23 console.log("Successfully connected to MongoDB");
24 })
25 .catch((error) => console.error(error));
26
27mongoose.connection.on("error", (err) => {
28 console.error("Connection to MongoDB interrupted, attempting to reconnect");
29});
30
31app.get("/", (req, res) => {
32 res.send("Hello, world!");
33});
34
35app.listen(port, () => {
36 console.log(`Server running at: http://localhost:${port}`);
37});
Now, we are going to configure a strategy we'd like to use for the Passport tokenization.
Create a folder in the root of your project directory called config
, then inside it
create a file called passport.js
.
At the top of that file, require the following:
1const JwtStrategy = require("passport-jwt").Strategy;
2const ExtractJwt = require("passport-jwt").ExtractJwt;
3const User = require("../models/user");
Then, export the following at the bottom of the file:
1const JwtStrategy = require("passport-jwt").Strategy;
2const ExtractJwt = require("passport-jwt").ExtractJwt;
3const User = require("../models/user");
4
5module.exports = function (passport) {
6 let opts = {};
7 opts.jwtFromRequest = ExtractJwt.fromAuthHeaderWithScheme("jwt");
8 opts.secretOrKey = "secret";
9
10 passport.use(
11 new JwtStrategy(opts, (jwt_payload, done) => {
12 User.getUserById(jwt_payload._id, (err, user) => {
13 if (err) {
14 return done(err, false);
15 }
16 if (user) {
17 return done(null, user);
18 } else {
19 return done(null, false);
20 }
21 });
22 }),
23 );
24};
You'll want to make sure you include the export above inside of your index.js file.
1require("dotenv").config();
2const passport = require("passport");
3require("./config/passport")(passport);
4const express = require("express");
5const path = require("path");
6const bodyParser = require("body-parser");
7const cors = require("cors");
8const mongoose = require("mongoose");
9mongoose.set("useUnifiedTopology", true);
10const app = express();
11const port = 3000;
12const users = require("./routes/users");
13
14app.use(cors());
15app.use(bodyParser.json());
16app.use("/users", users);
17
18app.use(passport.initialize());
19app.use(passport.session());
20
21mongoose
22 .connect(process.env.MONGODB_URI, { useNewUrlParser: true })
23 .then(() => {
24 console.log("Successfully connected to MongoDB");
25 })
26 .catch((error) => console.error(error));
27
28mongoose.connection.on("error", (err) => {
29 console.error("Connection to MongoDB interrupted, attempting to reconnect");
30});
31
32app.get("/", (req, res) => {
33 res.send("Hello, world!");
34});
35
36app.listen(port, () => {
37 console.log(`Server running at: http://localhost:${port}`);
38});
We should also import Passport.js and the JSON web token module into our routes/users.js
file:
1const express = require("express");
2const passport = require("passport");
3const jwt = require("jsonwebtoken");
4const router = express.Router();
5
6const User = require("../models/user");
7
8router.post("/register", (req, res, next) => {
9 res.send("REGISTER");
10});
11
12router.post("/authenticate", (req, res, next) => {
13 res.send("AUTHENTICATE");
14});
15
16router.get("/profile", (req, res, next) => {
17 res.send("PROFILE");
18});
19
20module.exports = router;
Now, you should save the files we were just editing and go check nodemon to go make sure that
nodemon isn't breaking. Once you've squashed any possible bugs, we are now going to our routes
directory into the users.js
file.
1const express = require("express");
2const passport = require("passport");
3const jwt = require("jsonwebtoken");
4const router = express.Router();
5
6const User = require("../models/user");
7
8router.post("/register", (req, res, next) => {
9 /**
10 * Note, there is nothing in this code to stop someone from registering
11 * the same email twice. This is just an example application to explain
12 * high level concepts, but if I were building this to be deployed into
13 * production, I would add a function to search the database
14 * for a user with the email in the request right here.
15 */
16 let newUser = new User({
17 name: req.body.name,
18 email: req.body.email,
19 username: req.body.username,
20 password: req.body.password,
21 });
22
23 newUser.save((err) => {
24 if (err) throw new Error("User did not save");
25 console.log("User saved", newUser);
26 res.status(201).send("User created!");
27 });
28});
29
30router.post("/authenticate", (req, res, next) => {
31 const email = req.body.email;
32 const password = req.body.password;
33
34 User.findOne({ email }, (err, user) => {
35 if (err) throw err;
36 if (!user) {
37 return res.json({ success: false, msg: "User not found!" });
38 }
39
40 user.comparePassword(password, (err, isMatch) => {
41 if (err) throw err;
42 if (isMatch) {
43 const token = jwt.sign({ user }, "secret", {
44 expiresIn: 604800, // 1 week in seconds
45 });
46
47 res.json({
48 success: true,
49 token: "JWT " + token,
50 user: {
51 id: user._id,
52 name: user.name,
53 email: user.email,
54 },
55 });
56 } else {
57 return res.json({ success: false, msg: "Wrong password!" });
58 }
59 });
60 });
61});
62
63router.get("/profile", (req, res, next) => {
64 res.send("PROFILE");
65});
66
67module.exports = router;
Quick explanation of the code above; first we're going to check if the email supplied with the
client-side request exists. If the email does exist, then we are going to take the password and try
to match it to the password associated with the user associated with the email in the database. If
the passwords match, we are going to assign the client an authentication token that expires in a
week, and then return some JSON containing a JSON Web Token ID to the request origin. If the
password does not match, the user is not authenticated and will have to re-enter their password.
We also created a function we have not yet defined, called User.comparePassword()
. Let's
go ahead and create this function in our User model, as to uphold the separation of concerns between
files we have stayed true to throughout this project and make sure everything is encapsulated
properly.
At the bottom of the models/user.js
file:
1const mongoose = require("mongoose");
2const bcrypt = require("bcryptjs");
3
4const UserSchema = mongoose.Schema({
5 name: {
6 type: String,
7 },
8 email: {
9 type: String,
10 required: true,
11 },
12 password: {
13 type: String,
14 required: true,
15 },
16});
17
18UserSchema.pre("save", function (next) {
19 var user = this;
20 if (!user.isModified("password")) return next();
21
22 bcrypt.genSalt(5, function (err, salt) {
23 if (err) return next(err);
24
25 bcrypt.hash(user.password, salt, function (err, hash) {
26 if (err) return next(err);
27 user.password = hash;
28 next();
29 });
30 });
31});
32
33UserSchema.methods.comparePassword = function (candidatePassword, cb) {
34 bcrypt.compare(candidatePassword, this.password, function (err, isMatch) {
35 if (err) return cb(err);
36 cb(null, isMatch);
37 });
38};
39
40module.exports = mongoose.model("User", UserSchema);
Alright, we should be okay now. That was quite a bit of code we just wrote, but let's go ahead and
try to see if it still works.
Open Postman, open a new tab and start drafting a new POST
request. the address is going
to be http://localhost:3000/users/authenticate
. For the body of the request:
1{
2 "username": "john",
3 "password": "123456"
4}
If all goes well, you should see something that looks like:
1{
2 "success": true,
3 "token": "JWT eyJHFJDKVjfkFHJKLEHVJKVnjkVNjkd39057JKDLFjkl7FJKDFLHJkhJkhfjekidvnrinuvdsjkl7853JHIELHuinjkfelnvuincivnsjkLuiHUGLnjKVLDNJNEIFLNVJDKn49329JKELNGiNJEKGRNnkef7FnjKVLn9FNJEK3klJdnJKGLRGLNGJKgNJkg7GjnklgrnjKLgejnkl",
4 "user": {
5 "id": "58a345a628f6455ab2912b2",
6 "name": "John Doe",
7 "username": "john",
8 "email": "jdoe@gmail.com"
9 }
10}
Aside: Why is the end point called /authenticate
and not /login
?
At the moment, all the /authenticate
endpoint does is return a JSON object with the JWT
token. In order to make the token useful so that the user's login session persists from request to
request, you'll need to tell the client to save that token in their auth headers with each request.
Alternatively, and more efficiently, you can store the token into either a cookie, or the localStorage object.
Conclusion
All in all, I really enjoyed this little project. I was so extremely frustrated after getting stuck
on this issue: https://github.com/matthewvolk/user-auth-node-app/issues/1 for a few days, only to
realize I had forgotten to import the actual passport module on a dependency file.
I'm not sure when I'll be finishing this project, as I have recently discovered the joys of Python.
I'm hoping to pick this back up sometime during the Summer of 2018.
Until then, stay RESTed ;)