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 4: Infrastructure for User. MongoDB database. MongoDB index. Jest testing for TypeScript. Your Settings page. API infrastructure for uploading file.

We keep our book up to date with recent libraries and packages.


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


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

We will cover the following topics in this chapter:

  • Infrastructure for User
    - User Schema and Model. Type interface.
    - Static methods and Mongoose methods
    - Express routes, router and API methods

  • MongoDB database
    - MongoDB Atlas
    - Creating MongoDB document
    - Connecting database
    - Testing connection

  • MongoDB index

  • Jest testing for TypeScript
    - generateSlug method
    - Testing generateSlug method

  • Your Settings page
    - Form and input
    - API infrastructure for updating profile

  • Uploading file API
    - Page method uploadFile
    - Getting signed request API
    - Env variables and CORS settings for uploading file
    - Uploading file using signed request API
    - Testing file upload
    - resizeImage


In Chapter 3, you learned and built "two-app" architecture. You learned new key concepts such as "API method" and "Express route". Here in Chapter 4, we will update our two-project architecture by adding a MongoDB database:
https://docs.atlas.mongodb.com/

In Chapter 3, we harcoded a user object inside an Express route at the api server

server.get('/api/v1/public/get-user', (_, res) => {
    res.json({ user: { email: 'team@builderbook.org' } });
});

In Chapter 4, instead of hardcoding a user object, we will actually retrieve data from the database.

After we connect our api server to a MongoDB database, the basic data flow (key request-response cycles) will look like this:
Builder Book

In addition to connecting a database, you will learn about the testing framework called Jest:
https://jestjs.io/

We will discuss when and why you would write tests for your code.

We will create a new page called YourSettings. This page shows basic information about your account and allows you to edit your name and avatar:
Builder Book

Finally, you will learn about building external, or third-party, API infrastructure. In Chapter 3, you built so-called internal API infrastructure that displayed a user's email address on the Index page.

An internal API infrastructure only invloves the api server and our database. An external API infrastructure involves an external, third-party server (and database). In this chapter, for example, we will build our first external API infrastructure - we will build infrastructure that uploads a file from our web application to the external AWS S3 service.


Infrastructure for User link

Connecting a MongoDB database to our api server is not enough for CRUDing (create, read, update, delete) data. We have to write methods that use MongoDB's API methods to CRUD (create, read, update, delete) data. In other words, we have to learn and implement MongoDB CRUD API:
https://docs.mongodb.com/manual/crud/

In this book, instead of using MongoDB CRUD API, we will use an abstraction layer for it, called mongoose:
https://mongoosejs.com/

In this section, we will get familiar with Mongoose API methds and use some of them for our firtst data model, User.

We will call a method that acts on a data model and uses a Mongoose API method internally, a static method.
Builder Book

In this section, we will work on the part colored red: Express routes, Static methods, and Mongoose methods.

In the next section, we will work on the part colored black: connecting api server to database.


User Schema and Model. Type interface. link

As we just mentioned, we need to get familiar with Mongoose API since, in this book, we chose not to work with a native Node.js driver for MongoDB:
https://mongoosejs.com/docs/guide.html

The mongoose library is an abstraction layer built on top of a MongoDB native driver. This means that whenever you call a Mongoose API method, you call a corresponding MongoDB method.

You might ask why you should learn a MongoDB abstraction instead of the native MongoDB driver. A good abstraction library delivers some value on top of the native library. If your data has no well-defined structure and you plan to write your own code to validate data types, then you can use MongoDB driver. However, if your data has well-defined structure (shape), then you should go with Mongoose. The native driver also has higher performance for Mongoose and provides you with a so-called Schema and Model in addition to MongoDB's Document:
https://developer.mongodb.com/article/mongoose-versus-nodejs-driver

The native driver can be significantly faster for some types of queries:
https://medium.com/@bugwheels94/performance-difference-in-mongoose-vs-mongodb-60be831c69ad

In this book, we use Mongoose to work with MongoDB.

MongoDB database stores data as a so-called Document. The format of the Document is BSON. BSON is a binary representation of JSON data:
https://docs.mongodb.com/manual/core/document/

Mongoose Schema allows you to define the shape of the MongoDB Document:
https://mongoosejs.com/docs/guide.html#definition

Simple example of Schema from the above link:

var mongoose = require('mongoose');
var Schema = mongoose.Schema;

var blogSchema = new Schema({
    title:  String, // String is shorthand for {type: String}
    author: String,
    body:   String,
    comments: [{ body: String, date: Date }],
    date: { type: Date, default: Date.now },
    hidden: Boolean,
    meta: {
        votes: Number,
        favs:  Number
    }
});

In Mongoose, a Model is a class:
https://mongoosejs.com/docs/api.html#model_Model
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

Class is a special type function (built-in in JavaScript). You can assign parameters and methods to the class. In the below example, height and width are parameters (also called properties), and calcArea is a method:

class Rectangle {
    constructor(height, width) {
        this.height = height;
        this.width = width;
    }
    // Method
    calcArea() {
        return this.height * this.width;
    }
}

const square = new Rectangle(10, 10);

console.log(square.height);
console.log(square.calcArea());

On your browser, open Chrome Developer Tools, navigate to the Console tab, and paste the above code:
Builder Book

You should see 10 and 100 printed in the browser console.

Besides parameters and methods that you define, class has special built-in methods - for example, the constructor method. A constructor is a special method that creates an object with some initial parameters or initial methods:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/constructor

In the above example, we defined the parameters height and width.

In the below example, we defined the parameter name (example from Mozilla docs):

class Polygon {
    constructor() {
        this.name = "Polygon";
    }
}

const poly1 = new Polygon();

console.log(poly1.name);
// expected output: "Polygon"

You created an object poly1 that has the parameter poly1.name with initial value of "Polygon".

You might use the constructor method if you are familiar with React. In React, if you need to set the initial state or bind some methods to this of a component or page component, you would use constructor:

constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
}

In fact, we used the constructor method when we defined the Notifier component inside our app project. Open book/4-start/app/components/common/Notifier.tsx and find this block:

constructor(props) {
    super(props);
    openSnackbarExternal = this.openSnackbar;
}

As you can see, we used constructor to bind (assign) the method openSnackbar to this, an instance of the component Notifier.

So a Mongoose Model is a class. How do we create it and extend it with Schema?

In Mongoose, we call the mongoose.model method to create a subclass of a Mongoose Model:

const User = mongoose.model('User', mongoSchema);

In Mongoose, the mongoose.model method creates a Model subclass using schema.

An instance of the User Model is a Mongoose Document, which is also a class. A Mongoose Document represents a one-to-one mapping to documents as stored in a MongoDB database:
https://mongoosejs.com/docs/documents.html

In the below example, user is an instance of the User Model, thus a Document:

const User = mongoose.model('User', mongoSchema);
const user = new User();

Since we are using TypeScript, we have to pass data types - we can use interface - to the above definition of User:

const User = mongoose.model<UserDocument, UserModel>('User', mongoSchema);

UserDocument and UserModel are interfaces:
https://www.typescriptlang.org/docs/handbook/interfaces.html

TypeScript uses interface to define data structure, parameters (also called properties), and functions (also called methods).

For example:

interface LabeledValue {
    label: string;
}

function printLabel(labeledObj: LabeledValue) {
    console.log(labeledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
// expected output: "Size 10 Object"

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

We keep our book up to date with recent libraries and packages.