Builder Book logo

Book: SaaS Boilerplate

  1. Introduction. Project structure.
  2. GitHub. VS Code Editor. Node. Yarn. TypeScript. TSLint. Next.js. Environmental variables.
  3. Material-UI. Theme. Dark theme. Shared layout. Shared styles. Shared components. Mobile browser.
  4. HTTP. APP server. Next-Express server. Fetch method. API methods. async/await. API server. Express server. Environmental variables. Logs.
  5. User model. Mongoose and MongoDB. MongoDB index. Jest testing. Your Settings page. File upload to AWS S3.
  6. Login page. Google OAuth API. Session and cookie. Authentication HOC withAuth.
  7. Google OAuth. Mailchimp.
  8. AWS SES. EmailTemplate. Welcome email.
  9. Passwordless OAuth.
  10. Team. Invitation. Invitation email.
  11. Stripe. Customer. Subscription. Invoice.
  12. Discussion. Post.
  13. Web sockets.
  14. Create Post via email. AWS Lambda. AWS API Gateway.
  15. Deploy to AWS Elastic BeanStalk.

Chapter 3: HTTP. APP server. Next-Express server. Fetch method. API methods. async/await. API server. Express server. Environmental variables. Logs.

Available for pre-order for $99. The price becomes $199 once published.


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
  • APP server
    - Step 1: Fetch method
    - Step 2: API method at Index page
    - Step 3: Next-Express server. Express route.
  • Asynchronous function, Promise, async/await
  • API server
    - New project API
    - Updating APP

In Chapter 2, we successfully integrated our Next.js web app with Material-UI and made many layout-related improvements. We discussed in detail two types of rendering in a Next.js web app: 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 some static HTML code with a few JavaScript methods, but we never had an API method that sent a request to the server to retrieve data there.

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 (client) to our web app's server
  • in return, the app's server sends a request to our database
  • once the app's server receives data, it sends a response with the attached data to the user's browser (client)

The above is just a simple example of data retrieval from a database. Once you finish this book, your final web application will have dozens of API methods to not only read data but also create, read, update, and delete data. The acronym to remember these 4 types of data manipulation is CRUD: create, read, update, delete.

In this chapter, we will only focus on reading the user's email from our application's server. We will talk about our MongoDB database in detail in Chapter 4.

link HTTP

Before we can discuss fetching data in our web application, we need to get familar 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 objects (sent by the client to the server) and response objects (sent by the server to the client in response to a request). The data can be an HTML document, image, or practically any other type of data.

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 object to the server. The server will process the request object and send back a response object that will have a body parameter that contains an email address.

The request object has multiple parameters. When we later construct our request object, we will include the following parameters in it: url, method, credentials, and headers (Content-Type and Cookie).

All possible parameters of a request object are here: https://developer.mozilla.org/en-US/docs/Web/API/Request

The parameters we will use are listed below.

url parameter: https://developer.mozilla.org/en-US/docs/Web/API/Request/url

method parameter: https://developer.mozilla.org/en-US/docs/Web/API/Request/method

credentials parameter: https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

headers parameter: https://developer.mozilla.org/en-US/docs/Web/API/Request/headers

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

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

When we receive a response object, we want to check the body property and parse the body's text as JSON.

response object: https://developer.mozilla.org/en-US/docs/Web/API/Response

body parameter: https://developer.mozilla.org/en-US/docs/Web/API/Body

link APP server

So now you know the theory of what we need to implement. 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. Write a method that creates a request object, sends this object to the server at a specified route (url; we call it route since we will send a request to the same website), receives a response object, and returns data (which is the JSON-parsed body of the response) that we requested.

  2. The method that we create in Step 1 will be used to create an API method. This API method should be executed when the end user loads the Index page. The API method should, in turn, call the above method from the Step 1 and return data to the page.

  3. Writing code to create a request object and send it to a particular route is not enough. We also have to write some code that detects a request at a particular route, retrieves this requested data, and returns a response object. We need to create a so-called Express route, a method that 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. We will not discuss our database in this chapter. We discuss MongoDB in detail in the Chapter 4. In this chapter, we will simply hardcode a value for user.email, say team@builderbook.org.

link Step 1: Fetch method

Our web application, on both server and client, is a JavaScript application. In JavaScript, there is a relatively new 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 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.

We dedicated a separate section in this chapter to explain Promise and async/await in detail. postData and fetch are asynchronous. async/await is newer syntax for asynchronous functions. 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 function with async function sendRequestAndGetResponse. Inside this function's definition, we will use await fetch.

Let's follow the above example for the asynchronous postData function and define our own asynchronous sendRequestAndGetResponse function. Create a new file book/3-begin/app/lib/api/sendRequestAndGetResponse.ts with 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(
        `${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, the client (browser) and the server do not support the fetch method. The package isomorphic-unfetch makes fetch globally available on the client and on the server:

https://www.npmjs.com/package/isomorphic-unfetch

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

Why do we need fetch on both client and server? Because our Next.js web application renders pages on both client and server. In Chapter 2, we discussed in detail when our Next.js app renders pages on the client and when on the server.

For a client-side rendered page, fetch runs on the browser, and the browser sends a request to the server.

For a server-side rendered page, fetch runs on the server, and the server sends a request to itself.

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

Let's look more closely into the above code:

  • The asynchronous function sendRequestAndGetResponse takes two arguments: the string path and object opts (options)

  • We construct a headers object from an empty object, whatever headers might be in opts (opts.headers) and Content-type header:

    const headers = Object.assign(
      {},
      opts.headers || {},
      {
          'Content-type': 'application/json; charset=UTF-8',
      },
    );

    We discussed the Object.assign method in Chapter 2. This method creates a new object from parameters of objects passed as arguments.

  • request is opts.request and headers.cookie is request.headers.cookie. In other words, we take the value of cookie from opts.request.headers.cookie and assign this value to a newly created headers object as the parameter headers.cookie.

  • query string qs is either opts.qs or an empty string

  • fetch takes two arguments: url which is ${path}${qs} and request which has parameters method, credentials, headers and opts:

    Object.assign({ method: 'POST', credentials: 'include' }, opts, { headers }),
  • We get text from the response's content with:

    const text = await response.text();
  • We need to decide what to do if the status of response is an error status. Go ahead and check up methods for response: https://github.com/developit/unfetch#response-methods-and-attributes

  • From the above link, we get:
    - response.status - contains the status code of the response, e.g. 404 for a not found resource, 200 for a success.
    - response.statusText - a message related to the status attribute, e.g. OK for a status 200.
    - response.text() - will return the response content as plain text.
    So if a status is 400 or higher (client errors: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status), we want to throw an error:

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

    Read about throw and Error. As you can see, throw new Error(response.statusText) creates an error object with the parameter message which has a value of response.statusText.

  • We JSON-parse the response with:

    const data = JSON.parse(text);
  • We return data unless try/catch (discussed in Chapter 2) returns an error in the try block. In that case, we throw error or return text:

    if (err instanceof SyntaxError) {
          return text;
      }
    
      throw err;
    }

For us to better understand data flow, let's add two console.log statements.


This chapter is under construction, you can pre-order this book for $99. The price after book's completion will be $199.

If you pre-order the book, you will be emailed about new chapters as they become available.

The book is to be completed by May 1, 2020.

Available for pre-order for $99. The price becomes $199 once published.


format_list_bulleted
help_outline
lens