Builder Book logo

Book: Builder Book

  1. Introduction
  2. Set up Node.js project. VS code editor and lint. Set up Next.js project. Material-UI integration. Server-side rendering. Custom styles.
  3. HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.
  4. Authentication HOC. getInitialProps method. Login page and NProgress. Asynchronous execution. Promise.then. async/await. Google Oauth API infrastructure. setupGoogle, verify, passport, strategy. Express routes /auth/google, /oauth2callback, /logout. generateSlug. this. Set up at Google Cloud Platform.
  5. Testing method with Jest. Transactional email API with AWS SES service. Set up AWS SES service, security credentials. sendEmail method. Export and import syntax for server code. EmailTemplate data model. Update User.signInOrSignUp. Informational success/error messages. Notifier component. notify method.
  6. Book data model. Chapter data model. MongoDB index. API infrastructure and user roles. Read chapter API.
  7. Set up Github API infrastructure. Sync content API infrastructure. Missing UI infrastructure for Admin user. Two improvements. Testing.
  8. Table of Contents. Sections. Sidebar. Toggle TOC. Highlight for section. Active section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book API infrastructure. Setup at Stripe dashboard and environmental variables. isPurchased and ReadChapter page. Redirect. My books API and MyBooks page. Mailchimp API.
  10. Prepare project for deployment. Environmental variables, production/development. Logger. SEO, robots.txt, sitemap.xml. Compression and security. Deploy project. Heroku. Testing deployed project. AWS Elastic Beanstalk.

Chapter 2: HTTP. Express server. Next-Express server, nodemon. Index.getInitialProps. User data model and mongoose. MongoDB database and dotenv. Testing server-database connection. Retrieving document. Session and cookie. MenuWithAvatar and Header components.

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.


The section below is a preview of Builder Book. To read the full text, you will need to purchase the book.


In Chapter 2, you will start with the codebase in the 2-begin folder of our builderbook repo and end up with the codebase in the 2-end folder. We will cover the following topics in this chapter:

  • HTTP

  • Express server
    - Next-Express server, nodemon
    - Index.getInitialProps
    - Testing new server

  • User data model and mongoose

  • MongoDB database and dotenv
    - Testing server-database connection
    - Retrieving document

  • Session
    - Configure session
    - Save session
    - Testing session and cookie

  • MenuWithAvatar and Header components


In the previous chapter (Chapter 1), we discussed our project structure, as well as Next.js, ESLint/Prettier formatting, HOCs, and server-side rendering. We also integrated our web application with Material-UI's library and added a few global and shared styles.

At this point in the book, our web application serves some static content on the Index page. Our web application does not have any internal or external API infrastructures to manage data. We don't have any dynamic data on the Index page, we don't CRUD data from a database, and a visiting user has no way to authenticate in our web application.

In this chapter, our main goals are to:
- Define an Express server
- Connect our server to a MongoDB database
- Create a session on the server, save a Session MongoDB document to our database, and save a corresponding cookie to a user's browser

At the end of this chapter, we will create a MenuWithAvatar component and make improvements to our Header component.

Start your app (yarn dev) and navigate to http://localhost:3000:
Builder Book

As you can see, our app is very basic and has no user authentication. The Index page is available to all users, and the Header component looks the same to all users. There is no way for a user to log in and see a unique dashboard page.

Before we can add user authentication to our app, we have to create a server and connect our app to a database. In a typical app, the server will receive a request from the client (a user's browser) to log in and search for a user inside a database. If a user already exists on the database, then the server will send that user's data to his/her browser. If a user does not exist on the database, then the server will create a new user.

Typically, the server listens for a request (req) from the client, executes some server-side functions, and then replies with a response (res). To better understand the concept of using a server, we should make a detour to understand the client-server protocol HTTP.


HTTP link

Before we create an Express server and before we define Express routes for various API endpoints, we have to understand the basic concept of HTTP.

HTTP (HyperText Transfer Protocol) is a client-server protocol, a system of rules that define how data is exchanged within or between computers (client and server). The client is usually the web browser. The server is one or more machines that serve data as a response (res) in response to request (req) from the client. HTTP is currently the most popular protocol on the web, you've probably noticed prepended http or https on web addresses:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview

Why is it important for us to understand HTTP? Because when building any data exchange for our web application, we will be thinking about it in terms of a request sent from the browser to the server and a response sent from the server to the browser.

We will discuss Express routes in more depth in Chapter 5. For now, you need to know that the browser sends a request object to a server at particular API endpoint. For example, consider a simple internal API infrastructure - Manage book API. We discuss and build this Manage book API infrastructure in Chapter 6. Let's summarize the chain of events that happen in Manage book API so that it becomes obvious why understanding the concepts of request and response is so important.
Builder Book

An Admin user (we discuss user roles later in the book) loads the AddBook page, fills out the form, and clicks the Save button. After clicking the button:
- On the user's browser, onSubmit of the form calls (though indirectly) the addBookApiMethod API method.
- On the user's browser, addBookApiMethod calls the fetch method, which sends a request via POST method with a new book's data inside the requests body to the API endpoint (or simply, route) at /api/v1/admin/books/add.
- On our web application's server, we have the Express route /api/v1/admin/books/add that waits for a request with the method POST.
- On our server, the Express route /api/v1/admin/books/add triggers a corresponding handler method that passes data from the request's body and calls the static method Book.add.
- Book.add creates (indirectly via the Mongoose API method create) a new Book MongoDB document in our MongoDB database.

You can see that about half of the events happen on the browser and the rest happen on the server, but the key communication event is when the addBookApiMethod API method sends an HTTP request to the Express route /api/v1/admin/books/add on the server. For every page loading and for every data CRUD-ing event in our web application - the browser needs to communicate with the server. Therefore, there must be an HTTP request-response cycle.

  • request is a HTTP message that is sent from the browser to the server:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
    In JavaScript, an object that corresponds to a request contains properties: method, path, version of protocol, and optional headers or body:
    Builder Book
    An HTTP method is an operation that the browser wants to perform. Most often, the browser gets data (say, a list of books) with the GET method or posts data (e.g creates a new book using the form's data) with the POST method. Other methods are available for more rare operations:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
    path is a relative route of the resource. Relative means that it does not include the protocol name (https://), main domain (say, builderbook.org), or port (443). In the example above, the path is /_next/cb2af84e5f28446e3dd58f1d53357d95/server.js.
    version of protocol is the version of the HTTP protocol - either HTTP/1.1 or HTTP/2.0. The latter is designed to have lower latency for the end user. Read more about it here.
    headers provide more descriptions to the server. Among the many parameters on the screenshot above, you'll notice dnt: 1. This parameter tells the server do not track. More on headers:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
    For example, in our second book, SaaS Boilerplate Book, we add an Access-Control-Allow-Origin header to the response from our server, because client-side and server-side applications are hosted on different origins:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
    body contains data. Typically, and in the context of our web application, we will use the body to send data with a request that uses the POST method (say, to create a new book, the req.body of such request will contain data such as the book's name, price, and githubRepo.). We will pass a small amount of data with an API endpoint's query or params when sending a request that uses the GET method (say, two URL queries, bookSlug and chapterSlug inside route /get-chapter-detail?bookSlug=${bookSlug}&chapterSlug=${chapterSlug}).
  • response is also an HTTP message, but it is sent from the server to the browser (the client):
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages
    A response contains version of protocol, status code, status message, and optional headers or body.
    Builder Book
    We already covered version protocol, headers, and body when discussing request.
    status code indicates whether a request succeeded or failed. status message is typically a one-word description that accompanies a status code.
    Take a look at our screenshot of a typical response. The response status 200 OK says that our request succeeded. Success means that the response's body contains the data that we requested with the GET method and path.
    If our request used the POST method instead, then 200 OK would mean that data inside the request's body was successfuly sent and received by the server.
    A full list of status codes is here:
    https://developer.mozilla.org/en-US/docs/Web/HTTP/Status

A note on the credentials property of request:
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

Later in this book, when we define the sendRequest method (used by all API methods), we will specify credentials: 'same-origin' for a request sent from browser to server. This option tells the browser to include user credentials (in our case, cookie) in the request if the URL is on the same origin as the calling script. Without sending cookie, our web application cannot have some useful UX features. We need cookie from a user's browser to have a truthy value of req.user on our server. Without req.user on our server, our web application cannot have a persistent session (user logs in and stays logged-in) and cannot display proper data on, for example, the ReadChapter and MyBooks pages.


Express server link

In the previous section, you learned the concepts of the client and the server and HTTP request and response. As we mentioned in the previous section, when discussing the Manage book API infrastructure, the key communication event is an HTTP request sent by an API method to an Express route. Express.js is the most popular server framework built on top of Node.js. In Chapter 5, we dive into a detailed discussion of Express routes, Express middleware, Express router, and properties and API methods of req and res (for example, req.params, req.body, res.redirect, res.json).

Consider the following example of the Express route logout (we will add this code in Chapter 3 when working on Google OAuth API infrastructure).

server/google.js :

server.get('/logout', (req, res, next) => {
    req.logout((err) => {
        if (err) {
            next(err);
        }
        res.redirect('/login');
    });
});

Take a careful look, as this code has all features of a basic Express route:
- It specifies a method for an incoming request (represented as req on the server); in this case method GET: server.get.
- It has a route/API endpoint: /logout.
- It executes a handler function - in this case, an anonymous arrow function: (req, res) => { ... }.
- It modifies req by calling the method req.logout and sends a response (represented as res on the server) that contains a directive to redirect a user to /login on the browser.

An Express server listens to requests from a browser using Express routes. When a user goes to a particular route on his/her browser or sends a request using an API method to a particular API endpoint, a handler function inside the matching Express route gets executed. Typically, in our project, an Express route will call some static method for a data model to CRUD (create/read/update/delete) data in our database. For example, on the Admin page, our web application will call the api/v1/admin/books Express route to get a list of all books. When an Admin user clicks the Save button on the AddBook page, our web application calls the api/v1/admin/books/add Express route to create a new Book MongoDB document. You will see many more examples of Express routes in the next chapters.

This introduction to Express routes will suffice to create and test our first Express server. We discuss Express routes and related concepts more deeply in Chapter 5.

Time to define our Express server. An example from the official docs:
https://expressjs.com/en/starter/hello-world.html

const express = require('express')
const app = express()
const port = 3000

app.get('/', (req, res) => {
    res.send('Hello World!')
})

app.listen(port, () => {
    console.log(`Example app listening at http://localhost:${port}`)
})

Create a new file, server/server.js, with the following content based on the above example:

const express = require('express');

const server = express();

server.get('/', (req, res) => {
    res.send('My express server');
});

server.listen(3000, () => {
    console.log('Ready on port 3000!');  // eslint-disable-line no-console
});

Start our Express server by running node server/server.js in your terminal when you are in the book/2-begin directory:

Navigate to http://localhost:3000 in your browser, and you will see a page with My express server!:
Builder Book

The method res.send sends an HTTP response. When the argument is a String, this method sets Content-Type to text/html, so the output is a page with the body My express server. Read more about this method in the official docs:
http://expressjs.com/en/api.html#res.send

Notice that we used:

const express = require('express');

Instead of the previously-used import syntax:

import express from 'express';

That's because server-only code does not support ES6 syntax for import, (Node will support eventually). Next.js compiles all code imported into pages, but code inside the server directory does not get imported into pages. Thus, we cannot use newer import syntax for server-only code.

If you use import express from 'express' and then run node server/server.js, you will see a syntax error in your terminal:

SyntaxError: Unexpected token import

You have two options: 1) you can use this new ES6 import syntax, but you have to compile your server code with babel-node; 2) you can use older syntax (require/modules.export). In this book, we chose to do the latter (older syntax), since compiling server code with babel-node brings us one step closer to configuration hell (getting an error due to being overwhelmed by many configurations).

We will import modules using require instead of import. Example:

const express = require('express');

We will export modules using modules.export and exports.X = X, instead of default export and export, respectively. Like so:

module.exports = User;

And so:

exports.setupGithub = setupGithub;

Next-Express server, nodemon link

We've just defined a simple Express server that responds with 'My express server' when the / route is loaded. Since we use the Next.js framework for our web application to render pages, we need to integrate our Express.js server with our Next.js server so that not only do Express routes work but also Next.js pages render. We'll closely follow this official example:
https://github.com/vercel/next.js/blob/canary/examples/custom-server-express/server.js

Code from the above example:

const express = require('express')
const next = require('next')

const port = parseInt(process.env.PORT, 10) || 3000
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
    const server = express()

    server.get('/a', (req, res) => {
        return app.render(req, res, '/a', ctx.query)
    })

    server.get('/b', (req, res) => {
        return app.render(req, res, '/b', ctx.query)
    })

    server.all('*', (req, res) => {
        return handle(req, res)
    })

    server.listen(port, (err) => {
        if (err) throw err
        console.log(`> Ready on http://localhost:${port}`)
    })
})

As you can see, we need to import a next instance and define a Next.js server, app, like this:


You've reached the end of the Chapter 2 preview. To continue reading, you will need to purchase the book.

We keep our book up to date with recent libraries and packages. Latest update Nov 2023.