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. 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. Data store for User. Toggle theme API. Team. Invitation.
  9. Discussion. Post. Web sockets.
  10. Stripe. Customer. Subscription. Invoice. Send email for new post. AWS Lambda. AWS API Gateway.
  11. Prepare APP and API for production. Deploy to Heroku. Deploy to AWS Elastic BeanStalk.

Chapter 4: User model. Mongoose and MongoDB. MongoDB index. Jest testing. Your Settings page. File upload to AWS S3.

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


The section below is a preview of this book, which is in progress. You can pre-order the book for $99. The price after book's completion will be $249.

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

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


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
    - Slugify method
    - Testing slugify method

  • Your Settings page
    - Form and input
    - Data validation

  • API infrastructure for uploading file
    - Page's uploadFile
    - Getting signed request API
    - Env variables for uploading file
    - Uploading file using signed request API
    - Testing file upload
    - resizeImage


In Chapter 3, you learned how to build an API infrastructure. You learned new concepts such as API methods and Express routes. Here in Chapter 4, we will connect our API project to a database, MongoDB:

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 for a user object from the database.

After we connect API to database, the basic data flow (request and 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 should 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 methods. An internal API method sends a request to our server. An external API method sends a request to a third-party, external server. In this chapter, we will send a request to an AWS S3 server to upload a file (a user's avatar) to that AWS S3 server.

link Infrastructure for User

If we create a MongoDB database and connect it to our API server, we won't be able to read or save data to that database. That's because in order to CRUD (create, read, update, delete) data from MongoDB, we have to learn and implement MongoDB's CRUD API:

https://docs.mongodb.com/manual/crud/

Thus in this section, we will get familiar with MongoDB's CRUD API and apply it to our User Model. In the next section, we will actually create a new database and connect it to the API server.

Look at the following diagram:

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: MongoDB and connection of MongoDB to API server.

link User Schema and Model. Type interface.

As we just mentioned, we need to get familiar with MongoDB's CRUD API. Using native MongoDB's API is straightforward - you would just install a native Node.js driver locally to your machine:

https://docs.mongodb.com/ecosystem/drivers/node/

And follow API documentation:

https://docs.mongodb.com/manual/crud/

However, in this book, we chose not to work with a native Node.js driver for MongoDB. Instead, we decided to show you how to work with a MongoDB abstraction: Mongoose. Abstraction means that Mongoose is built on top of a MongoDB native driver. Although Mongoose has its own API documentation, Mongoose is built on top of MongoDB. This means that whenever you call a Mongoose 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. You should choose the native MongoDB driver if your data has no well-defined structure and you plan to write code to validate data types. However, if your data has well-defined structure (shape), then you should go with Mongoose. Mongoose provides you with a so-called Schema and Model in addition to MongoDB's Document.

In this book, we use Mongoose to work with MongoDB. If your data does not have well-defined shape, then you are welcome to learn and use the native MongoDB driver.

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

Simple example of Schema from Mongoose docs:

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, 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). 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 (also 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 a 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.

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

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

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

In Mongoose, the 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 MongoDB:

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 - using 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"

Interface LabeledValue defines the shape of the object labeledObj.

When TypeScript compiles code, it will check if labeledObj, an argument that is passed to the function printLabel, has the parameter/property label. TypeScript will also check that the data type of label is a string.

In our case, we use two interfaces: UserDocument and UserModel. We use them to define the shape of Document and Model, because when we call the mongoose.model method, we want to check that all parameters (properties) are present and are the proper data type.


The section above is a preview of this book, which is in progress. You can pre-order the book for $99. The price after book's completion will be $249.

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

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

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


format_list_bulleted
help_outline
lens