Advanced Routing and MongoDB

Modern JavaScript

Advanced Routing and MongoDB

As ExpressJS applications grow in complexity, it becomes necessary to organize your routes into separate files. This allows you to break your application into smaller, more manageable pieces, and makes it easier to maintain and update your code.

In this unit, we will learn how to create a modular ExpressJS application that uses MongoDB to store and retrieve data.

Modular Routing

In the previous unit, we created a simple ExpressJS application that contained all of our routes in a single file. This works fine for small applications, but as your application grows, it can become difficult to manage all of your routes in a single file.

To solve this problem, we can break our routes into separate files and organize them into a folder structure that makes sense for our application. Take a look:

Moving Static Routes

In this video we will move our static (front-end) routes to a separate file and folder.

Show/Hide Video

Here is our new folder structure:

.
├── public
|   ├── scripts
│   │   └── site.js
│   ├── styles
│   │   └── site.css
│   └── index.html
├── routes
│   └── static.js
├── app.js
├── endpoints.rest
└── package.json

And here is our new static.js file:

const path = require('path')
const router = require('express').Router()

const root = path.join(__dirname, '..', 'public')

router.get('/', (request, response) => {
    response.sendFile('index.html', { root })
})

router.get('/pokemon/:id', (request, response) => {
    response.sendFile('index.html', { root })
})

router.get('/type/:type', (request, response) => {
    response.sendFile('index.html', { root })
})

module.exports = router

And here is the code that we added to our app.js file:

// attach endpoints
app.use(require('./routes/static'))

Moving API Routes

In this video we will move our API routes to a separate file and folder.

Show/Hide Video

Here is our new folder structure for the routes:

└── routes
    ├── api
    │   └── v1
    │       └── pokemon.js
    └── static.js

Here is the code that we added to our pokemon.js file:

const router = require('express').Router()

const pokemon = [
    { id: 1, name: 'Bulbasaur', type: 'Grass' },
    { id: 2, name: 'Ivysaur', type: 'Grass' },
    { id: 3, name: 'Venusaur', type: 'Grass' },
    { id: 4, name: 'Charmander', type: 'Fire' },
    { id: 5, name: 'Charmeleon', type: 'Fire' },
    { id: 6, name: 'Charizard', type: 'Fire' },
    { id: 7, name: 'Squirtle', type: 'Water' },
    { id: 8, name: 'Wartortle', type: 'Water' },
    { id: 9, name: 'Blastoise', type: 'Water' },
]

router.get('/random', (request, response) => {
    const r = Math.floor(Math.random() * 9)
    response.send(pokemon[r])
})

router.post('/add', (request, response) => {
    const { id, name, type } = request.body
    console.log({ id, name, type })

    const found = pokemon.find(p => p.id.toString() === id.toString())
    if (found) response.send({ error: { message: `Pokemon with id: ${id}, already exists`} })
    else pokemon.push({ id, name, type })
})

router.get('/:id', (request, response) => {
    const { id } = request.params
    const found = pokemon.find(p => p.id.toString() === id)
    if (found) response.send(found)
    else response.send({ error: { message: `Could not find pokemon with id: ${id}` }})
})

router.get('/random/:type', (request, response) => {
    const { type } = request.params
    const found = pokemon.filter(p => p.type.toLowerCase() === type.toLowerCase())
    const r = Math.floor(Math.random() * found.length)
    if (found.length > 0) response.send(found[r])
    else response.send({ error: { message: `Could not find pokemon with type: ${type}` }})
})

module.exports = router

And here is the code that we added to our app.js file:

app.use('/api/v1/pokemon', require('./routes/api/v1/pokemon'))

Note

The more specific routes should be placed before the more general routes. This is because ExpressJS will match the first route that it finds that matches the request.

MongoDB

MongoDB is a popular NoSQL database that is used by many developers to store and retrieve data.

Register and Create a Database

In this video, we will register for a free MongoDB Atlas account and create a new project and database.

Show/Hide Video

Here is the link to Register for MongoDB Atlas .

Adding Data to the Database

Next we will add a few pokemon to our database, and then install the mongodb package using npm.

Show/Hide Video

To install the mongodb package, run the following command:

npm install mongodb@6.3.0

Connection Setup

Next let's setup our project to connect to our MongoDB database.

Show/Hide Video

Here is the code for the dbconnect.js file:

const { MongoClient, ObjectId } = require('mongodb')

const { uri } = require('./secrets/mongodb.json')

const client = new MongoClient(uri)

Retrieving Data from MongoDB

In this video, we will retrieve data from our MongoDB database and display it in our ExpressJS application.

Show/Hide Video

Here is the updated dbconnect.js file:

const { MongoClient, ObjectId } = require('mongodb')

const { uri } = require('./secrets/mongodb.json')

const client = new MongoClient(uri)

const getCollection = async (dbName, collectionName) => {
    await client.connect()
    return client.db(dbName).collection(collectionName)
}

module.exports = { getCollection, ObjectId }

And here is the updated endpoint in our pokemon.js file:

router.get('/:number', async (request, response) => {
    const { number } = request.params
    const collection = await getCollection('PokemonAPI', 'Pokemon')
    console.log(await collection.findOne({ "number": parseInt(number) }))
    response.send('done')
})

Updating our GET endpoints

In this video, we will update our GET endpoints to retrieve data from our MongoDB database.

Show/Hide Video

Here are the updated endpoints in our pokemon.js file:

router.get('/random', async (_, response) => {
    const collection = await getCollection('PokemonAPI', 'Pokemon')
    const count = await collection.countDocuments()
    const number = Math.floor(Math.random() * count) + 1
    const found = await collection.findOne({ "number": parseInt(number) })
    if (found) response.send(found)
    else response.send({ error: { message: `Could not find pokemon with number: ${number}` }})
})

router.get('/:number', async (request, response) => {
    const { number } = request.params
    const collection = await getCollection('PokemonAPI', 'Pokemon')
    const found = await collection.findOne({ "number": parseInt(number) })
    if (found) response.send(found)
    else response.send({ error: { message: `Could not find pokemon with number: ${number}` }})
})

router.get('/random/:type', async (request, response) => {
    const { type } = request.params
    const collection = await getCollection('PokemonAPI', 'Pokemon')
    const foundOfType = await collection.find({ "type": type }).toArray()
    const count = foundOfType.length
    if (count === 0) response.send({ error: { message: `Could not find pokemon with type: ${type}` }})
    const number = Math.floor(Math.random() * count)// + 1 <-- error in the video
    //console.log(number, foundOfType)
    response.send(foundOfType[number])
})

Note

There was a small error in the video regarding the calculation for the random number. The "+ 1" should be removed from the calculation.

Updating our POST endpoint

In this video, we will update our POST endpoint to add data to our MongoDB database.

Show/Hide Video

First we made a change to how we got the connection:

let collection = null
const getPokemon = async () => {
    if (!collection) collection = await getCollection('PokemonAPI', 'Pokemon')
    return collection
}

Then we updated our POST endpoint:

router.post('/add', async (request, response) => {
    const { number, name, type } = request.body
    const collection = await getPokemon()
    const { acknowledged, insertedId } = await collection.insertOne({ number, name, type })
    response.send({ acknowledged, insertedId })
})

Getting an Item by ID

In this video, we will update our GET endpoint to retrieve data from our MongoDB database by ID.

Show/Hide Video

Here is the new endpoint in our pokemon.js file:

router.get('/byId/:id', async (request, response) => {
    const { id } = request.params
    const collection = await getPokemon()
    const found = await collection.findOne({ _id: new ObjectId(id) })
    if (found) response.send(found)
    else response.send({ error: { message: `Could not find pokemon with id: ${id}` }})
})

Exercise 1

Show/Hide Video

For this exercise, you will create and test two new endpoints:

  1. GET /api/v1/pokemon/ - This endpoint should retrieve all of the pokemon from the database.

    • Hint: Use the find method without any parameters to retrieve all of the pokemon.
  2. GET /api/v1/pokemon/byName/:name - This endpoint should retrieve a pokemon from the database by name. Additionally, I want you to use the following query parameters, to make the search case-insensitive:

const regexp = new RegExp(`^${name}`, 'i')
const found = await collection.findOne({ name: regexp })

Then test your endpoints using the endpoints.rest file.

Hints

How do I use the `find` method to retrieve all of the pokemon?

You can use the find method without any parameters like this:

const found = await collection.find().toArray()

Solution

Show the Answer

The "GET /api/v1/pokemon/" endpoint should look like this:

router.get('/', async (_, response) => {
    const collection = await getPokemon()
    const found = await collection.find().toArray()
    response.send(found)
})

The "GET /api/v1/pokemon/byName/:name" endpoint should look like this:

router.get('/byName/:name', async (request, response) => {
    const { name } = request.params
    const collection = await getPokemon()
    const regexp = new RegExp(`^${name}`, 'i')
    const found = await collection.findOne({ name: regexp })
    if (found) response.send(found)
    else response.send({ error: { message: `Could not find pokemon with name: ${name}` }})
})

Here are the tests that you can use in the endpoints.rest file:

### Get all pokemon
GET {{url}}/pokemon/

### Get pokemon by name
GET {{url}}/pokemon/byName/ivysaur
Walkthrough Video