Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. Setup. GitHub and Git. Visual Studio code editor. Node, Yarn. package.json. TypeScript. ESLint, Prettier. Next.js. Server-side rendering. Project structure. Document HOC. App HOC. Index page. Testing. Environmental variables.
  3. Material-UI. Client-side and server-side rendered pages. Dark theme, CssBaseline. Shared layout. Adding styles. Shared components. MenuWithLinks. Notifier. Confirmer. Nprogress. Mobile browser.
  4. HTTP, request, response. APP project. Fetch method. API method at Index page. Next-Express server. Express route. Asynchronous function, Promise, async/await. API server. New project API. Updating APP.
  5. Infrastructure for User. MongoDB database. MongoDB index. Jest testing for TypeScript. Your Settings page. API infrastructure for uploading file.
  6. Login page. Session and cookie. Google OAuth API. Authentication HOC withAuth. firstGridItem logic in App HOC.
  7. AWS SES API. Passwordless OAuth API. Mailchimp API.
  8. Application state, App HOC, store and MobX. Toggle theme API. Team API. Invitation API.
  9. Discussion API. Post API. Websockets for Discussion and Post.
  10. Stripe API - API project. Stripe API - APP project. Setup at Stripe dashboard and environmental variables. Email notification for new post API - API project. Amazon API Gateway and AWS Lambda.
  11. Environmental variables, production/development. Logger. APP server. API server. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. Testing application in production. AWS Elastic Beanstalk.

Chapter 3: HTTP, request, response. APP project. Fetch method. API method at Index page. Next-Express server. Express route. Asynchronous function, Promise, async/await. API server. New project API. Updating APP.

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.


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


In Chapter 3, you will start with the codebase in the 3-begin folder of our saas repo and end up with the codebase in the 3-end folder.

We will cover the following topics in this chapter:

  • HTTP, request, response

  • APP project
    - Step 1: Fetch method
    - Step 2: API method at Index page
    - Step 3: Next-Express server. Express route.

  • Asynchronous execution, callback, Promise, async/await

  • API server
    - New project API
    - Updating APP


In Chapter 2, we successfully integrated our Next.js web application with Material-UI and made many layout-related improvements. We discussed in detail two types of rendering in a Next.js web application: server-side and client-side. We set and ran many tests to understand differences between server-side and client-side rendered pages. However, in those tests, our pages did not contain data fetched from the server. We had static HTML code, but we never had an API method that sent a request to the server to CRUD (create, read, update, delete) data.

The simplest example of data retrieval can be described like this: An end user loads a page of our web application. That page contains some static HTML and CSS code that shows layout and styles. On this page, our goal is to show the user some data in addition to the static code. The data could be the user's picture or email, so the user knows that he/she is properly authenticated on this page. To implement such a scenario, we have to:
- Create an API method that sends a request from the user's browser to our server
- In return, our server sends a request to our database
- Once our server receives data from our database, it sends a response with the attached data to the user's browser

The above is just a simple example of data retrieval that may happen for a client-side rendered page. Once you finish this book, your final web application will have dozens of API methods to not only read data but also do other data transformations.

In this chapter, we will only focus on reading the user's email from our application's server. The main focus is on two-project architecture. Normally, with one-project architecture, we have browser-side and server-side code inside the same project. With two-project architecture (app and api), we will have app that primarily has pages' code. This code can run on both browser and server, depending on the type of rendering (client-side or server-side rendering). app is either on the browser or the server, and app calls API methods that send requests to the api server. Code at the api project is server-only. The api server sends a request to MongoDB's server to CRUD data from our database, then sends a response to app, which is either on browser or server depending on the type of rendering.

Here is a typical internal API infrastructure and request-response cycles in our SaaS boilerplate:
Builder Book

We will discuss later the difference between internal and external API infrastructure (or API for short).

In the section below, we will learn about the concepts of HTTP request and HTTP response.


HTTP, request, response link

Before we can discuss fetching data in our web application, we need to get familiar with the basic concepts of HTTP, request, and response.

Simply put - HTTP (HyperText Transfer Protocol) is a set of rules (protocol) that governs data exchange on the web. These rules specify how a client (typically a web browser, called client because it is served data by a server) and server (typically a web server, a machine that sends data to a client) exchange messages. These messages are request (sent by the client to the server) and response (sent by the server to the client in response to a request). The data can be an HTML document, image, JSON or practically any other type of data.

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 use so-called API methods to send requests from the browser to the server, and we will use so-called Express routes on the server to get data and respond back to the browser.

In this chapter, our goal is to set up infrastructure to display a user's email address on the Index page. We want to write code in the following way: When a user loads the Index page, the user's browser (the client) sends a request to the server. The server will process the request and send back a response that will have a body parameter that contains an email address.

Both request and response are HTTP messages:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages

request is an HTTP message that is sent from the browser to the server:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#http_requests

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#http_responses


The request has multiple properties. In JavaScript, an object that corresponds to a request can contain these properties among other: version of protocol, path, method, credentials, and headers (Content-Type and Cookie headers).
https://developer.mozilla.org/en-US/docs/Web/API/Request

Builder Book

The properties we will specify are listed below.

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.

url or path property. 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:
https://developer.mozilla.org/en-US/docs/Web/API/Request/url

method property. 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/API/Request/method

credentials property controls whether the client (the browser) attaches cookie (Cookie header with value) to request. The server can use cookie to identify a unique session and user. For example, cookie can be used to create acpersistent session for a logged-in user. Check up possible values for the credentials property:
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

headers provide more descriptions to the server. Among the many properties on the screenshot above, you'll notice dnt: 1. This parameter tells the server do not track.

On Do Not Track header:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT

On Content-Type header:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type

On Cookie header:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie

request has an optional body property (not all requests and responses have one):
https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#body

req.body contains data and is typically used by a request with the method POST to send data from the client to the server. For example, when an end user creates a new Post or Discussion in our web application. For a request with the method GET, we don't specify body to send data. Typically, we will use req.query to send string-type data - for example, slug.


A response contains version of protocol, status code, status message, and optional headers or body.
https://developer.mozilla.org/en-US/docs/Web/API/Response

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

In Chapter 4, when we add cors configuration to our server (api project), our server will add Access-Control-Allow-Origin header to the server's response. This response header controls whether requesting code can access a response from the server. Since our app and api projects will be served at different domains, setting up cors configuration that allows app access to a response from api is a must:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin


APP project link

So now that you know about HTTP request and HTTP response, we can write our first API method that sends a request and receives a response from the server.

We can start making changes to the app project at the book/3-begin location. Our final goal is to display the email address of a user when the user loads the Index page.

We can split our entire task into three parts:

  1. Call the fetch method with a constructed request object and route (same website for now, later to a different website) as arguments. Receive a response object and return data (which is the JSON-parsed body of the response). Define sendRequestAndGetResponse using the fetch method.

  2. Define an API method using the sendRequestAndGetResponse method. This API method should be executed when the end user loads the Index page. The API method calls the sendRequestAndGetResponse method, which in turn, calls the fetch method. The final result is the Index page getting data from the response object's body property.

  3. On the server, we need some way to detect an incoming request at a particular route. We should also retrieve requested data and send it back to requesting code as a response. Since we plan to use an Express server, such construct is called an Express route. A handler function of an Express route gets executed once our Express server receives a request with a matching route. Typically, the goal of an Express route is to retrieve data from a database and send a response with the data attached. Since we are not discussing database in this chapter, we will simply hardcode the requested data inside the Express route.


Step 1: Fetch method link

Our app web application, on both the server and the client, is a JavaScript application. In JavaScript, you can use the fetch method to send a request to the server and to receive a response. The method is called fetch:
https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch

From the above documentation:

// Example POST method implementation:
async function postData(url = '', data = {}) {
    // Default options are marked with *
    const response = await fetch(url, {
        method: 'POST', // *GET, POST, PUT, DELETE, etc.
        mode: 'cors', // no-cors, *cors, same-origin
        cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
        credentials: 'same-origin', // include, *same-origin, omit
        headers: {
            'Content-Type': 'application/json'
            // 'Content-Type': 'application/x-www-form-urlencoded',
        },
        redirect: 'follow', // manual, *follow, error
        referrerPolicy: 'no-referrer', // no-referrer, *client
        body: JSON.stringify(data) // body data type must match "Content-Type" header
    });
    return await response.json(); // parses JSON response into native JavaScript objects
}

postData('https://example.com/answer', { answer: 42 })
    .then((data) => {
        console.log(data); // JSON data parsed by `response.json()` call
    });

What do we see here?
- fetch takes the url string and request object as arguments
- fetch returns a response object
- the function postData has async in front of it, and fetch has await in front of it

Both the postData and fetch method are asynchronous - that means each of these functions returns a Promise.

In JavaScript, code executes from top to bottom. But if there is some kind of error in the middle of the code, execution will stop at that point. To avoid this, JavaScript offers the asynchronous function. A code block with an asynchronous function does not need to complete execution - the rest of the code can still be executed in synchronous fashion.

Why do we need an asynchronous function? Imagine we built a SaaS boilerplate where fetching data was not an asynchronous operation. Every time a user sends a data request to the server, this user and our web application won't be able to do anything else on our web application. The web application will not function while the browser is waiting for data. Imagine multiple users blocking each other.

We dedicated a separate section of this chapter to explain Promise and its syntactic sugar async/await in detail. postData and fetch are asynchronous. Usage of await is optional for an asynchronous function. In the above example, we defined the asynchronous postData function and added await in front the fetch method. It's important to remember that await can be added only in front of another asynchronous function - in our case, this function is fetch. Adding await makes JavaScript pause on the line that has await until, in this case, the asynchronous method fetch returns data.

Later in this subsection, we will create an asynchronous method with async function sendRequestAndGetResponse. Inside this method's definition, we will use await fetch.

Let's follow the above example for the asynchronous postData function and define our own asynchronous sendRequestAndGetResponse method. Create a new file, book/3-begin/app/lib/api/sendRequestAndGetResponse.ts, with the following content:

import 'isomorphic-unfetch';

export default async function sendRequestAndGetResponse(path, opts: any = {}) {
    const headers = Object.assign(
        {},
        opts.headers || {},
        {
            'Content-type': 'application/json; charset=UTF-8',
        },
    );

    const { request } = opts;
    if (request && request.headers && request.headers.cookie) {
        headers.cookie = request.headers.cookie;
    }

    const qs = opts.qs || '';

    const response = await fetch(
        `${process.env.URL_APP}${path}${qs}`,
        Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }),
    );

    const text = await response.text();

    if (response.status >= 400) {
        throw new Error(response.statusText);
    }

    try {
        const data = JSON.parse(text);

        return data;
    } catch (err) {
        if (err instanceof SyntaxError) {
            return text;
        }

        throw err;
    }
}

Why do we need to add and import a new dependency, isomorphic-unfetch?

By default, not all clients (browsers) and servers support the fetch method. The package isomorphic-unfetch makes fetch globally available in our code on both the client and on the server:
https://www.npmjs.com/package/isomorphic-unfetch

This package switches between unfetch and node-fetch for the browser and the server, respectively.

Why do we need fetch on both client and server? Because our Next.js web application can render pages on both the client and the server. In Chapter 2, we discussed in detail when our Next.js web application renders pages on the client and when on the server - it depends on how a user accessed the page.
- For a client-side rendered page, fetch runs on the browser, and the app code on the browser sends a request to the app code on the server.
- For a server-side rendered page, fetch runs on the server, and the app code on the server sends a request to itself.

Thus, we need to make sure that the fetch method is available on both the client and the server.

Let's look more closely into the above code:


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

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.