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 request
s 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 arequest
contains properties:method
,path
,version of protocol
, and optionalheaders
orbody
:
An HTTPmethod
is an operation that the browser wants to perform. Most often, the browser gets data (say, a list of books) with theGET
method or posts data (e.g creates a new book using the form's data) with thePOST
method. Other methods are available for more rare operations:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Methodspath
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, thepath
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 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-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-Originbody
contains data. Typically, and in the context of our web application, we will use thebody
to send data with arequest
that uses thePOST
method (say, to create a new book, thereq.body
of suchrequest
will contain data such as the book'sname
,price
, andgithubRepo
.). We will pass a small amount of data with an API endpoint'squery
orparams
when sending arequest
that uses theGET
method (say, two URL queries,bookSlug
andchapterSlug
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
Aresponse
containsversion of protocol
,status code
,status message
, and optionalheaders
orbody
.
We already coveredversion protocol
,headers
, andbody
when discussingrequest
.status code
indicates whether a request succeeded or failed.status message
is typically a one-word description that accompanies astatus code
.
Take a look at our screenshot of a typical response. The response status200 OK
says that our request succeeded. Success means that the response's body contains the data that we requested with the GETmethod
andpath
.
If ourrequest
used the POST method instead, then200 OK
would 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 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.