Builder Book

  1. Introduction
  2. App structure. Next.js. HOC. Material-UI. Server-side rendering. Styles.
  3. Server. Database. Session. Header and MenuDrop components.
  4. Authentication HOC. Promise. Async/await. Static method for User model. Google OAuth.
  5. Testing with Jest. Debugging with Winston. Transactional emails. In-app notifications.
  6. Book and Chapter models. Internal API. Render chapter.
  7. Github integration. Admin dashboard. Testing Admin UX and Github integration.
  8. Table of Contents. Highlight for section. Hide Header. Mobile browser.
  9. BuyButton component. Buy book logic. ReadChapter page. Checkout flow. MyBooks page. Mailchimp API. Deploy app.

Chapter 2: Server. Database. Session. Header and MenuDrop components.

We keep the book up-to-date with the latest frameworks and packages.


In Chapter 2, you will start with the codebase in the 2-start 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
    - Nodemon
    - Index page
    - Testing

  • Database
    - dotenv
    - Testing connection

  • Session
    - Configure session
    - Save session
    - Testing

  • Update Header component

  • MenuDrop component


In the previous chapter (Chapter 1), we discussed our app structure, as well as Next and server-side rendering. We also integrated our app with Material-UI and added a few global and shared styles.

At this point, our app is simply a static app - meaning the app does not have a server that receives requests (req) and returns responses (res). In this chapter, our main goals are to:

  • create an Express server
  • connect our app to a database (MongoDB)
  • set up and customize session

At the end of this chapter, we will create a MenuDrop 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.

link HTTP

To properly set up our server, we have to understand how HTTP's request/response, headers, and methods work.

HTTP (HyperText Transfer Protocol) is a client-server protocol, a system of rules that define how data is exchanged within or between computers. Client is usually the web browser. Server is one or more machines that serve data as a response (res) to a request (req) from a client. HTTP is currently the most popular protocol on the web.

1) Request is a message that is sent from client to server. A request contains method, path, version of protocol, and optional headers or body.

Builder Book

An HTTP method is an operation that the client wants to perform. Most often, the client gets data (e.g. a list of books) with GET or posts data (e.g creates a new book) with POST. Other methods are available for more complicated operations.

path is a relative route of the resource. Relative means that it does not include the protocol name (https://), main domain (builderbook.org), or port (443). In the example above, the path is /_next/cb2af84e5f28446e3dd58f1d53357d95/app.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 (parameters) 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.

body contains data. In the context of our app, we will use the POST method to create a new book. The body of this request will contain data such as the book's name, price, and githubRepo.

2) Response is a message sent from server to client. 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 if 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 correct 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.

A note on credentials - in Chapter 5, we will write code that sends a request to our server. Among other parameters, such as method type, we will specify credentials: same-origin. This option tells the client to include user credentials (e.g. cookie) in the request, providing that the request is sent to the same domain as the location of the script that sends the request (read docs).

link Express server

In the previous section, you learned the concepts of client and server, request and response, and the HTTP methods GET and POST (we will use only these two methods to write all of our Express middleware and routes).

Express is the most popular framework built on top of Node.

In Chapter 5, when we dive into internal APIs, we will discuss Express middleware and routes in detail. In this section, let's understand how a simple Express route works on a high level.

Consider the following example of an Express route (we will write this code in Chapter 3):
server/google.js :

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

Take a careful look, as this code has all features of a basic Express route:

  • It has the route /logout
  • It executes the function: (req, res) => { ... }
  • It modifies req and/or res. In this case, both of them: req.logout(); and res.redirect('/login');
  • It uses the HTTP method GET: server.get()

An Express server listens to requests from a client using Express routes. When a user goes to a particular route on his/her browser, a function in an Express route that matches the client route gets executed.

Typically, in our app, an Express route will call some static method for a particular Model to CRUD (create/read/update/delete) data. For example, on the Admin dashboard, our app calls the /books Express route to get a list of all books. When an Admin clicks the Add book button, our app calls the books/add Express route to create a new book document. You will see more examples in Chapter 5.

This introduction to Express routes will suffice to create our first Express server. However, if you'd like to learn more, read the official docs or check Chapter 5, section Internal APIs.

Time to create our Express server.

Create a server/app.js file with the following content.
server/app.js :

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 your express server by running node server/app.js. Navigate to http://localhost:3000 in your browser, and you will see a page with My express server!:

Builder Book

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 Express docs.

Notice that we used:

const express = require('express');

instead of:

import express from 'express';

That's because Node does not support ES6 syntax for import (Node will support it soon).

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

SyntaxError: Unexpected token import

You can use this new ES6 import syntax, but you have to compile your server code with babel-node. If you ran yarn at the beginning of this chapter, then you installed babel-node as a part of the babel-cli package.

To compile with babel-node, you would run npx babel-node file.js. Read about its usage in the official docs. In our case, the command is:

npx babel-node server/app.js

Let's test it out:

  1. Replace const express = require('express'); with import express from 'express';
  2. Start the server with yarn nodemon server/app.js --watch server --exec babel-node --presets=@babel/preset-env instead of node server/app.js.

The app starts successfully. Thus, we can use ES6 import syntax in babel-compiled server code.

link Nodemon

Currently, to see changes in our browser after we edit server/app.js, we have to stop and start our server manually. This is a big time waste. If, for example, we want to change a route inside our Express route from / to /aaa, we'd like to see this change on the browser without restarting our server manually.

To save time, we should restart the server automatically when a file changes - Nodemon does exactly that.

If you ran yarn at the beginning of this chapter, then you installed and added nodemon to devDependencies.

Go to your terminal and run:

yarn nodemon server/app.js --exec babel-node --presets=@babel/preset-env

Navigate to http://localhost:3000. Now edit the server/app.js file - for instance, change the text inside the response to My Express server 101. Go back to your browser, refresh the tab, and you will see your changes (without restarting your server manually):

Builder Book

By default, nodemon will watch for file changes in the current directory. Let's ask nodemon to watch for changes in our server/* directory. To do so, append the command with --watch server:

yarn nodemon server/app.js --watch server --exec babel-node --presets=@babel/preset-env

To save time, add a new command called yarn dev-express to the script section of our package.json file:

"scripts": {
    "build": "next build",
    "start": "next start",
    "dev": "next",
    "lint": "eslint components pages lib server",
    "dev-express": "nodemon server/app.js --watch server --exec babel-node --presets=@babel/preset-env"
},

The babel-node command compiles server code (all code that is imported to server/app.js) with Babel. As we discussed in Chapter 1, we need to make sure that browsers understand the code we wrote. If you try to run code without compiling, the terminal will print out an error. Try starting your app with node instead of babel-node:

nodemon server/app.js --watch server --exec node

The terminal will print out:

SyntaxError: Unexpected token import

This error indicates that Node does not support newer ES6 import/export syntax. We compile all code that is imported to pages with Babel (used by Next.js internally). Server code, though not run on browsers, needs to be understood by Node. Server code is not compiled by Next.js - thus we use babel-node to compile server code so we are able to use newer syntax such as import/export.

Again, as mentioned in Chapter 1, Babel does not transform code by default. Thus we specify our preset with --presets=@babel/preset-env. This preset compiles ES6 (ES2015) or newer syntax down to ES5.

To compile server code with Babel, we use @babel/core, @babel/node, and @babel/preset-env packages. These packages are installed if you ran yarn at the beginning of this chapter.

Use your new command to start the server - yarn dev-express instead of yarn dev.

To reduce confusion when we start our express server, we will serve our app at port 8000 instead of the Next.js default port 3000. To do so, let's define the port variable: server/app.js :

import express from 'express';
const port = process.env.PORT || 8000;
const ROOT_URL = `http://localhost:${port}`;
const server = express();

server.get('/', (req, res) => {
    res.send('My express server');
});
server.listen(port, () => {
    console.log(`> Ready on ${ROOT_URL}`);  // eslint-disable-line no-console
});

If you set up Eslint properly (we did it in Chapter 1), then the line with console.log() will be highlighted with an Eslint warning/error: [eslint] Unexpected console statement. (no-console). You can disable this Eslint error by adding // eslint-disable-line no-console to the line that contains console.log(). Read more about disabling Eslint errors.

At this point, we've created an Express server. Since we use the Next framework for this app, our actual goal is to configure a Next server. We'll closely follow this official example.

In short, we need to import a Next server:

import next from 'next'

Then, we need to pass NODE_ENV to this Next server (production or development). The boolean parameter dev is true when the enviroment is not production and false when the environment is production:

const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });

The official example defines a handler function as:

const handle = app.getRequestHandler()

and uses this function for GET requests:

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

    server.get('/', (req, res) => {
        res.send('My express server');
    });
    server.get('*', (req, res) => handle(req, res));
    server.listen(port, (err) => {
        if (err) throw err;
        console.log(`> Ready on ${ROOT_URL}`); // eslint-disable-line no-console
    });
}

In summary, we get: ./server/app.js :

import express from 'express';
import next from 'next';
const port = process.env.PORT || 8000;
const ROOT_URL = `http://localhost:${port}`;
const dev = process.env.NODE_ENV !== 'production';
const app = next({ dev });
const handle = app.getRequestHandler();

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

    server.get('/', (req, res) => {
        res.send('My express server');
    });
    server.get('*', (req, res) => handle(req, res));
    server.listen(port, (err) => {
        if (err) throw err;
        console.log(`> Ready on ${ROOT_URL}`); // eslint-disable-line no-console
    });
});

If you set up Eslint properly (we did it in Chapter 1), then the line with console.log() will be highlighted with an Eslint warning/error:

[eslint] Unexpected console statement. (no-console)

You can disable this Eslint error by adding // eslint-disable-line no-console to the line that contains console.log(). Read more about disabling Eslint errors.

To continue reading, buy this book.

We keep the book up-to-date with the latest frameworks and packages.


format_list_bulleted
help_outline