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.
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:
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.
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.
requestis 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 arequestcontains properties:method,path,version of protocol, and optionalheadersorbody:
An HTTPmethodis an operation that the browser wants to perform. Most often, the browser gets data (say, a list of books) with theGETmethod or posts data (e.g creates a new book using the form's data) with thePOSTmethod. Other methods are available for more rare operations:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methodspathis 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, thepathis/_next/cb2af84e5f28446e3dd58f1d53357d95/server.js.version of protocolis 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.headersprovide more descriptions to the server. Among the many parameters on the screenshot above, you'll noticednt: 1. This parameter tells the serverdo 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 anAccess-Control-Allow-Originheader 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-Originbodycontains data. Typically, and in the context of our web application, we will use thebodyto send data with arequestthat uses thePOSTmethod (say, to create a new book, thereq.bodyof suchrequestwill contain data such as the book'sname,price, andgithubRepo.). We will pass a small amount of data with an API endpoint'squeryorparamswhen sending arequestthat uses theGETmethod (say, two URL queries,bookSlugandchapterSluginside route/get-chapter-detail?bookSlug=${bookSlug}&chapterSlug=${chapterSlug}).responseis 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
Aresponsecontainsversion of protocol,status code,status message, and optionalheadersorbody.
We already coveredversion protocol,headers, andbodywhen discussingrequest.status codeindicates whether a request succeeded or failed.status messageis typically a one-word description that accompanies astatus code.
Take a look at our screenshot of a typical response. The response status200 OKsays that our request succeeded. Success means that the response's body contains the data that we requested with the GETmethodandpath.
If ourrequestused the POST method instead, then200 OKwould mean that data inside therequest'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!:
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 importYou 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.