Basic Node Express API Part 2

Continuation from Part 1

Check out Part 1 for the introduction and basic setup here.

Setup the Database

Create a database, or deployment, on mLab.

Select AWS as the cloud provider and sandbox as the plan type.

Then choose a region closest to you.

Finally, give it a name and you’ll have your own MongoDB database on the cloud for free!

Click on your newly created database and then go to the Users tab.

Add a database user and give them a username and password (don’t make them read-only).

Create a folder inside your project config folder and then create a db.js file.

# inside /example-api using the terminalmkdir config            // create the folder
cd config // move into that folder
touch db.js // create a javascript file

“To connect using a driver via the standard MongoDB URI”

Grab the second script, this quoted one above, on the database web page on how to connect to the database and paste it into your db.js file as follows.

# db.jsmodule.exports = {
url: "mongodb://<dbuser>:<dbpassword>@ds11111.mlab.com:11111/example-api"
};
// replace <dbuser> with the username you created, and not your login into mLab, but the user you created in Step 2
// replace <dbpassword> too
// replace ds11111 and 11111/example-api
// ensure it looks like your script just with the username, password, numbers, and project name

Create a collection, or a table, for Users. You can do so under the Collections tab and by clicking the + Add Collection button.

We’re only dealing with users and the whole user account setup (refer back to the CRUD section in the previous post).

Let’s finish the database setup in server.js! Add the following to start using MongoDB with your project.

# server.jsconst MongoClient = require('mongodb').MongoClient
const db = require('./config/db')
const dbName = 'example-api'

Now to actually connect the project and the database:

# server.jsMongoClient.connect(db.url, { useNewUrlParser: true }, (err, client) => {const db = client.db(dbName)if(err) return console.log(err)require('./routes')(app, db)app.listen(PORT, () => {
console.log(`app is up and running captain!`)
})
})

*Notice we moved the earlier code inside this connect function

Can’t have a RESTful API without routes!

Before we get to routes, let’s ensure our API has everything to deal or assist with it:

# server.js// gain access to necessary tools
const bodyParser = require('body-parser')
const morgan = require("morgan")
const cors = require('cors')
const cookieParser = require('cookie-parser')
// cors setup--dev environment anyways
const corsOptions = {
origin: '*'
}
app.use(morgan("short"))
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
app.use(cors(corsOptions))
app.use(cookieParser())

Get familiar with the following:

# routes.jsmodule.exports = function(app, db) {
const usersCollection = db.collection('Users');

app.get('/', (req, res) => {
res.send("hello world");
});
};

You can use the good ol’ HTTP requests: GET, POST, PATCH, DELETE with:

app.get()
app.post()
app.patch()
app.delete()

Their first argument is the path or the route, and the second is a callback function for HTTP requests and responses. Requests meaning the ones coming from the client or users, and responses referring to the server sending information back.

We did GET already, so let’s do POST next.

Here we are taking in a username and a password from the request’s body and checking the database to see if there is a user going by that username already — if it is, then notify the user to try a different username, otherwise it will create that username for them by saving it in the database along with the password.

# routes.jsapp.post('/signup', (req, res) => {
const { username, password } = req.body

// check to see if the username already exists
usersCollection.findOne({ username: username.toLowerCase() }, (err, item) => {

if(err) {
console.log(err)
res.end()
}
if(item) {
res.json({ status: 401, msg: 'Username unavailable! :(' })
}
const newUser = { username, password }usersCollection.insertOne(newUser, (err, result) => {

if(err) {
res.json({ error: err })
}
res.json({ status: 200, msg: `${username} created!`})
})
})
})

*You can read up on destructuring here, was originally around for arrays but more recently now capable with objects

*MongoDB documentation for available commands

Can’t have signing up functionality without logging in, so let’s get to it:

# routes.jsapp.post('/login', (req, res) => {const { username, password } = req.bodyusersCollection.findOne({ username: username.toLowerCase() }, (err, user) => {

if(err) {
res.json({ error: err })
}
if(user) {if(user.password === password) {
res.json({ status: 200, msg: 'login successful' })
}
}
res.json({ status: 401, msg: 'incorrect username or password '})
})
})

Basically the same as registering a user but with a password comparison and without the creating an account.

We’ll get back to CRUD later, let’s setup the rest of the routes for now.

# routes.jsapp.patch('/users/:id', (req, res) => {})
app.delete('/users/:id', (req, res) => {})

Since we are serving our local server on port 3000 — we now have the following RESTful routes:

http://localhost:3000/signup
http://localhost:3000/login
http://localhost:3000/users/:id

Password Encryption

We’re going to create a function to encrypt inputs since we’ll be using it a couple of times to keep our code DRY.

# example-api directory
touch encrypt.js
# encrypt.jsconst bcrypt = require('bcrypt')
const saltRounds = 10
module.exports = (password) => {
const salt = bcrypt.genSaltSync(saltRounds)
const hash = bcrypt.hashSync(password, salt)
return hash
}

Now that we have the encryption function, let’s apply this to our existing user creating function:

# routes.js// from const newUser = { username, password } to:const newUser = { username, password: encrypt(password) }

With this you’ll see a password change from something like:

unencrypedpassword

to:

$2b$10$C/16FoPT9tOQWFKNs7uAa53LTkdIBJIAVPn1hpO9po9qIYfRUC

Time to adjust our login functionality too!

# routes.js/* from 
if(user.password === password) {
res.send({ status: 200, msg: 'login successful' })
}
*/
// tousersCollection.findOne({...
...
if(user) {
...
bcrypt.compare(password, user.password, (err, result) => {
if(result) {
res.json({ status: 200 })
}
}
}

res.json({ status: 401, msg: 'incorrect username or password' })
})

Persisting a user session

or keeping a user logged in even as they navigate around the web application or simply come back later after having had closed the window earlier

As mentioned in the previous post, you can do this one of two ways:

  • localStorage
  • cookies

Anyone can access both in the browser by opening the console and typing in some simple JavaScript code. But it is a little more difficult to do this if you set a cookie with httpOnly properties, and even more so if you also include secure.

If we add the following to our existing signup and login functions, we can easily create a user session, or persist a user:

# routes.js--# app.post('/signup') && app.post('/login')localStorage.setItem('token', user.id)// or with cookiesres.cookies('token', user.id)

These are insecure as anyone can access them like mentioned previously, and anyone can spoof their user account or user session by manually changing the value of the token to any other valid id number.

Therefore we need to securely store this information, but also ensure users can’t easily change it to a valid value. And as mentioned earlier, we will achieve this with JWT .

*I pronounce it like, “to jot something down” but I’ve heard J-W-T before too.

We will need a secure secret passphrase that will be used to encode and decode the hashed information. In order to do so we will create an environment file and store it in there.

*Be sure to include it in your .gitignore file if you will be uploading to GitHub by simply typing the relative path including the filename

Create a file called .env in the root of the project directory.

# .env// key = value
SECRET = whatisthesecrettolifeanyways?

Since we’re wonderful folks we will be using this function in at least two places — login and signup. Therefore we will create a function to be used in both places and keep our code DRY.

Create a file called cookies.js and include the following:

# cookies.jsconst jwt = require("jsonwebtoken")
require('dotenv').config()
const secret = process.env.TOKEN_SECRET
const setCookie = (id, res) => { const payload = { id } // use jwt to encode the payload, or id in this case
const token = jwt.sign(payload, secret, { expiresIn: "2 days" })
// set the cookie in the response to be sent back to the user which will be automatically saved in the browser under Application in the developer tools window
res.cookie("token", token, { httpOnly: true })
}
const removeCookie = res => { // delete the token from the user's browser upon logout
res.clearCookie("token")
}
// make these two functions available in other files
module.exports = {
set: setCookie,
remove: removeCookie
}
/*
named it set and remove so when we import it in another file, it will look like the following
cookies.set()
cookies.remove()
*/

As said in the last part of the previous step, let’s import this new function and put it to work!

# routes.jsconst cookies = require('./cookies')# signup and login post routes, before sending a responsecookies.set(user["_id"], res)

Now when a user logs in or signs up on our web application, they will be signed in for 2 days before needing to sign back in if they navigate away or close their browser.

With that said, they can manually delete the cookie in the browser of course. This is why when you delete your cookies, you have to log back in on other websites!

We’ll put the remove function to use in step 5.

Now that we can save a cookie with secure information in order to persist a user session, we have to be able to retrieve the cookie, decode it, and verify that it’s valid.

Since this is a useful functionality and it will be used in multiple places, let’s create a verifyToken.js file.

# verifyToken.jsconst jwt = require("jsonwebtoken")
const secret = process.env.TOKEN_SECRET
const verifyToken = (token) => { let result; jwt.verify(token, secret, (err, decoded) => { if (err) {
result = err
}
result = decoded }) return result}module.exports = verifyToken

Simply returning decoded didn’t work so I decided to go with this workaround by setting it to a variable and returning it afterwards.

You can create a custom middleware as an argument with your routes to check to see if a user is already logged in and if they’re not you can send them a message stating so, redirect them, or outright deny them access to your application.

Start by creating said middleware, checkAuth.js.

# checkAuth.jsconst jwt = require("jsonwebtoken")
const secret = process.env.TOKEN_SECRET
const checkAuth = (req, res, next) => { const token = req.cookies.token || req.body.token || req.headers['x-access-token'] || req.query.token try { if (!token) {
res.redirect('/app/login.html')
}
if (!!req.route.path.match(/login/) || !!req.route.path.match(/signup/)) { res.redirect('/app/index.html')
}
jwt.verify(token, secret, (err, decoded) => { if (err) {
res.send('Unauthorized')
}
next() }) } catch (error) {
console.log(`checkAuth error:`, error)
}
}
module.exports = checkAuth

Then in routes.js we can apply this to any and all routes as follows:

# routes.jsconst checkAuth = require('./checkAuth')app.get('/', checkAuth, (req, res) => {
res.send("hello world! you're authenticated to see this!")
})

Now for the logging out capability — if you already haven’t guessed, it goes something like this:

# routes.jsapp.delete('/logout', (req, res) => {  cookies.remove(res)  res.json({ msg: "Successfully logged out" })})

Since we decided to not repeat ourselves and to prevent future typos we separated a cookies function and in order to actually remove a cookie from the user’s browser, we require access to response so we pass it as an argument. And that’s that.

{ Update } = CRUD

Since we’re only working with users, let’s implement a basic function to update a user’s information on their account by:

# routes.jsapp.patch('/users/:id', (req, res) => {  const user = req.params.id
const userProperties = req.body
usersCollection.updateOne(user, { $set: userProperties }, (err, item) => { if (err) {
res.send({ status: 400, msg: 'something went wrong, please try again.' })
}
res.send({ status: 200, msg: 'Update successful!' })
})
})

{ Delete } = CRUD

We already have one delete HTTP method, but it can’t hurt to have another right? This time it’s to completely remove a user’s account from the database.

# routes.jsconst ObjectID = require("mongodb").ObjectID
app.delete('/users/:id', (req, res) => { const token = req.cookies.token
const decoded = verifyToken(token)
const userId = { "_id": new ObjectID(req.params.id) }
usersCollection.deleteOne(userId, (err, item) => { if (err) {
res.status(400).send({ status: 400, msg: err })
}
cookies.remove(res) res.status(200).send({ status: 200, msg: 'User successfully deleted!' })
})
})

Can’t forget to also delete the cookie if the user is destroying their own account, we cannot continue to let them stay logged in — plus that would create a disconnect between their browser and the database anyways.

External Fetching

Last but not least we’ll go over how we can use the Fetch API to make external requests to other APIs. We’ll use Unsplash for their pictures collection and free API. They require users of their API to be authenticated via an API key, so we’ll store that sensitive data in the environment file as a variable there.

# .envUNSPLASH_API_KEY = *******************************

Next we will do the actual info fetching!

Imagine your front end application wants some pictures based on a user search. When they type something in and press enter, that request will hit this new route — on your API — which will send a request to the Unsplash API.

Your API will then use that data and send it back to your front end which will display it on the page for the user to be visually stunned.

# routes.jsconst fetch = require('node-fetch')
const splashKey = process.env.UNSPLASH_API_KEY

app.get('/search/:query', (req, res) => {
const query = req.params.query
const url = `https://api.unsplash.com/search/?photos?page=1&query=${query}`
const options = {
method: 'GET',
headers: {
'Authorization': `Client-ID ${splashKey}`,
},
}
fetch(url, options)
.then(r => r.json())
.then(data => {
const imageLinks = data.photos.results.map(el => el.urls.regular); res.json(imageLinks)
})
})

And that’s how you can do several things with a node and express API!

Full-Stack Web Developer - Former JPM Analyst, ESL Teacher, and Expat