[flaviocopes] The Node.js Course - Node.js Trip Planner

I recommend downloading my Node.js Handbook and using that as a companion book to the course.
This is the recommended courses order. You are free to jump through them, but the first ones are generally easier than the ones that follow.

I recommend to keep those resources of mine
at hand while following the course:

Introduction to the project

Our task is to create a trip cost calculator app.

Imagine going on a trip, and you have your app (which can be a progressive web app, or a mobile app for example) where you add any expense you make. Gasoline, hotels, food, tickets and so on.

When the trip ends, you archive it and it becomes part of the history - which you can navigate and see how much you spent in the past trips.

We’re not going to create the frontend of the application here, as this course is focused on Node.js and I don’t want to dilute the concepts.

What we’re going to build is the API for this project, using Express and MongoDB .

Define what we need to do

In the introduction you saw the overview of what we’ll build. We need to manage a list of trips, and we need to track expenses for each of those trips.

Let’s now dissect this into details, and translate it into a series of API endpoints.

An endpoint is a unique URL we will call to create an operation.

Like adding a new trip with its name.

If you are unfamiliar with how HTTP works, take the time to read my tutorials on the HTTP protocol and how HTTP requests work.

At the beginning, there is no trip stored, and we need to add one. I imagine the app will ask the user for the name, and there will be a “Create trip” button. When clicked, the app will send the name to us, to the /trip endpoint with the POST HTTP method.

We have our first endpoint, which will accept a name property.

POST /trip { name }

Another endpoint will list the trips, and it’s

GET /trips

By default, it will return the trips ordered by creation date.

When the user wants to add a new expense, the app will invoke the /expense endpoint with the POST method, with some parameters that describe the expense

POST /expense { trip, date, amount, category, description }

trip is the ID of the currently selected trip.

category is the name of the category of the expense. We’ll provide a list of categories to choose from, which is static: travel , food , accomodation , fun .

When we want to retrieve a trip expenses, we call the /expenses endpoint with the GET method:

GET /expenses { trip }

passing the trip identifier.

Let’s start the project

I am going to use a local installation of Node.js.

We start our Node.js project by going into a new folder (call it tripcost ) and typing the command npm init .

Answer the questions (you can also just press enter to get the default values, or use npm init --yes to skip them), and you are ready to go.

We’ll use MongoDB as our database.

If you are unfamiliar with MongoDB, check out my tutorials and install MongoDB on your system:

Done? Great! Install the Node.js package right now with

npm install mongodb

While you’re here, also install Express:

npm install express

Create a server.js file now, where we’ll store our API code, and start requiring Express and MongoDB:

const express = require('express')
const mongo = require('mongodb').MongoClient

Initialize the Express app:

const app = express()

And now we can add the stubs for the API endpoints we support:

app.post('/trip', (req, res) => { /* */ })
app.get('/trips', (req, res) => { /* */ })
app.post('/expense', (req, res) => { /* */ })
app.get('/expenses', (req, res) => { /* */ })

Finally, use the listen() method on app to start the server:

app.listen(3000, () => console.log('Server ready'))

You can run the application using node server.js in the project folder.

Project up to now: https://glitch.com/edit/#!/node-course-project-tripcost-a?path=server.js

Adding trips

We offer the client a way to add trip using the POST /trip endpoint:

app.post('/trip', (req, res) => { /* */ })

Let’s go ahead and implement it.

We already included the MongoDB library, so we can use it in our endpoint implementation.

const mongo = require('mongodb').MongoClient

Next, we build the MongoDB server URL. If you are running the project locally, and MongoDB locally too, the URL is likely this:

const url = 'mongodb://localhost:27017'

because 27017 is the default port.

Next, let’s connect to the database using connect() :

let db

mongo.connect(url, (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db('tripcost')
  }
)

and while we’re here, let’s also get a reference to the trips and expenses collections:

let db, trips, expenses

mongo.connect(url, (err, client) => {
    if (err) {
      console.error(err)
      return
    }
    db = client.db('tripcost')
    trips = db.collection('trips')
    expenses = db.collection('expenses')
  }
)

Now we can go back to our endpoint.

This endpoint expects 1 parameter, name , which represents how we call our trip. For example Sweden 2018 or Yosemite August 2018 .

See my tutorial on how to retrieve the POST query parameters using Express

We expect data coming as JSON, using the Content-Type: application/json , so we need to use the express.json() middleware:

app.use(express.json())

We can now access the data by referencing it from Request.body :

app.post('/trip', (req, res) => {
  const name = req.body.name
})

Once we have the name, we can use the trips.insertOne() method to add the trip to the database:

app.post('/trip', (req, res) => {
  const name = req.body.name
  trips.insertOne({ name: name }, (err, result) => {
  })
})

We handle the error, if present in the err variable, otherwise we send a 200 response (successful) to the client, adding an ok: true message in the JSON response:

app.post('/trip', (req, res) => {
  const name = req.body.name
  trips.insertOne({ name: name }, (err, result) => {
    if (err) {
      console.error(err)
      res.status(500).json({ err: err })
      return
    }
    console.log(result)
    res.status(200).json({ ok: true })
  })
})

That’s it!

Now restart the Node application by hitting ctrl-C to stop it, and run it again.

You can test this endpoint using the Insomnia application, a great way to test and interact with REST endpoints:

Project up to now https://glitch.com/edit/#!/node-course-project-tripcost-b?path=server.js

List the trips

The list of trips is returned by the GET /trips endpoint. It accepts no parameters:

app.get('/trips', (req, res) => { /* */ })

In the previous lesson we initialized the trips collection, so we can directly access that to get the list. We use the trips.find() method, which result we must convert to an array using toArray() :

app.get('/trips', (req, res) => {
	trips.find().toArray((err, items) => {

	})
})

Then we can handle the err and items results:

app.get('/trips', (req, res) => {
	trips.find().toArray((err, items) => {
    if (err) {
      console.error(err)
      res.status(500).json({ err: err })
      return
    }
    res.status(200).json({ trips: items })
	})
})

Here’s the result of the API call in Insomnia:

Project up to now: https://glitch.com/edit/#!/node-course-project-tripcost-c?path=server.js

Add an expense

In the previous lesson we got the list of trips. Every trip has an associated _id property which is added by MongoDB directly when it’s added:

{
	"trips": [
		{
			"_id": "5bdf03aed64fb0cd04e15728",
			"name": "Yellowstone 2018"
		},
		{
			"_id": "5bdf03c212d45cdb5ccec636",
			"name": "Sweden 2017"
		},
		{
			"_id": "5bdf047ccf4f42dc368590f6",
			"name": "First trip"
		}
	]
}

We’ll use this _id to register a new expense.

If you remember, the endpoint to add a new expense is this:

POST /expense { trip, date, amount, category, description }

trip in this case will be the _id of one of the trips we previously registered. Imagine that in the app, the user will add one trip, and that will remain the current trip until a new one is added (or selected).

Let’s go ahead and implement our stub:

app.post('/expense', (req, res) => { /* */ })

Like when adding a trip, we’re going to use the insertOne() method, this time on the expenses collection.

We get 5 parameters from the request body:

  • trip
  • date the date, in ISO 8601 format (e.g. 2018-07-22T07:22:13 ), in the GMT timezone
  • amount an integer with the amount
  • category which is one from travel , food , accomodation , fun
  • description a description for the expense, so we’ll remember about it later
app.post('/expense', (req, res) => {
  expenses.insertOne(
    {
      trip: req.body.trip,
      date: req.body.date,
      amount: req.body.amount,
      category: req.body.category,
      description: req.body.description
    },
    (err, result) => {
      if (err) {
        console.error(err)
        res.status(500).json({ err: err })
        return
      }
      res.status(200).json({ ok: true })
    }
  )
})

The code at this point is available at https://glitch.com/edit/#!/node-course-project-tripcost-d?path=server.js

List all expenses

The last piece of the puzzle is getting the expenses.

We need to fill the /expenses endpoint stub, the last one missing:

app.get('/expenses', (req, res) => {
  /* */
})

This endpoint accepts the trip parameter, which like in the last lesson when we added an expense, it’s the _id property of a trip stored in the database.

app.get('/expenses', (req, res) => {
  expenses.find({trip: req.body.trip}).toArray((err, items) => {
    if (err) {
      console.error(err)
      res.status(500).json({ err: err })
      return
    }
    res.status(200).json({ trips: items })
  })
})

This Insomnia screenshot shows it at work:

You can find the complete code available at https://glitch.com/edit/#!/node-course-project-tripcost-e?path=server.js

Wrapping up and challenges

You can clone the project at https://github.com/flaviocopes/node-course-project-tripcost and implement the following coding challenges. Once you are done, you can post your result in Discord and ask for feedback!

  • Create a frontend using plain JavaScript, or any frontend framework you are familiar with, like Vue or React. Skip if you are not interested in the frontend side.
  • The API now does not implement authentication. Read my tutorial on JWT authentication and implement authentication for the API, so you can have more than one user. Trips would be assigned to a specific user, so expenses are as well, automatically.