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 API. Post API. Websockets.
  10. Stripe API and paid subscription. Email notification for new post. AWS Lambda. AWS API Gateway.
  11. Environmental variables, production/development. Logger. API server. Server-side caching. SEO - robots.txt, sitemap.xml. Server-side caching. Heroku. AWS Elastic Beanstalk.

Chapter 6: AWS SES API. Passwordless OAuth API. Mailchimp API.

Last week to pre-order for $99. The price becomes $199 at midnight on Sunday, August 16th.


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 $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 August 16, 2020.


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

We will cover the following topics in this chapter:

  • AWS SES API
    - EmailTemplate model, insertTemplates and getEmailTemplate methods
    - Adding getEmailTemplate and sendEmail to signInOrSignUpViaGoogle
    - sendEmail method
    - Setting up AWS SES and EMAIL_SUPPORT_FROM_ADDRESS
    - Testing AWS SES API
  • Passwordless OAuth API
    - Configure passwordless
    - Configure passwordless token
    - Express routes for Passwordless API
    - signInOrSignUpByPasswordless method for User model
    - Testing Passwordless OAuth API
  • Mailchimp API
    - callAPI method
    - addToMailchimp method
    - Adding addToMailchimp to signInOrSignUpViaGoogle and signInOrSignUpByPasswordless
    - Environmental variables for Mailchimp API
    - Testing Mailchimp API

In this chapter, we have three major features to add to our SaaS boilerplate:
- the ability to send emails (AWS SES API)
- allowing end users to sign up or log in using passwordless authentication (Passwordless OAuth API)
- saving email addresses to a Mailchimp list (Mailchimp API)

We will not only discuss how to build these features in detail but also discuss why you might want these feature in your SaaS business.


link AWS SES API

One common feature of modern web applications is the ability to send emails. In Chapter 5, we added user authentication using Google OAuth. Nowadays, a typical internet user signs up in many web applications. It is a good idea to send a welcome email to a newly signed-up user. When an end user logs into our web application for the first time, our API server, in addition to creating a new User document in our database, should send a welcome email to this user.

Besides welcoming a new user, sending emails might be useful for other situations. By the end of this book, you will set up the following emails in your SaaS boilerplate:
- welcome email sent by API server to a newly signed-up user (this section)
- login email sent by API server to let a user log into our web application using the passwordless method (next section of this chapter)
- invitation email sent by API server to invite a new user to join an existing team (Chapter 7)
- newPost email sent by AWS Lambda function sendEmailForNewPost to notify a user about a new Discussion or new Post inside an existing Discussion (Chapter 11)

Your business may have more use cases for sending emails. For example, in our SaaS product Async, we use AWS SES API to let Async's users create a new Post by simply replying to a newPost email.

This is the high-level outline of how we want our welcome email to work:
- A new end user clicks the login button on the login page of our web application on the browser
- Some method in our API server retrieves an email template for a welcome email from our MongoDB database
- Some other method if our API server takes all necessary parameters and passes them to an AWS SES API method that sends a request to the AWS SES server.
- The AWS SES server sends a request with an email message to the end user's Email server
- The newly signed-up end user can see our welcome email on the browser, inside the end user's inbox web application

To better understand req-res cycles, let's display the above description on a diagram:

Builder Book

This is how we want AWS SES API to work in more detail:
- On the browser, a new end user clicks the LoginButton component on the Login page of our web application
- This triggers an entire cascades of redirects, methods, and req-res cycles
- Eventually, the static method signInOrSignUpViaGoogle of our User model gets called
- Inside the signInOrSignUpViaGoogle method, we will call two methods: getEmailTemplate and sendEmail
- getEmailTemplate will search for the welcome email template inside the emailtemplates collection of our MongoDB database
- if getEmailTemplate successfully finds the welcome email template, it adds variable parameters to the template (in our case, the user's displayName)
- next, the sendEmail method gets called with multiple arguments (email template is one of those arguments) - sendEmail calls the ses.sendEmail AWS SES API method:

https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html#sendEmail-property

  • ses.sendEmail sends a request with the POST method from our API server to the AWS SES server
    - The AWS SES server sends an email to an Email server. In our case, it is Gmail server.
    - A newly signed-up user finds our welcome email inside their inbox web application on the browser. In our case, we will be checking our Gmail inbox on the browser.

In the next few subsection of this section, we will implement every step from the above detailed description.


link EmailTemplate model, insertTemplates and getEmailTemplate methods

In this section, we will define a new Mongoose model: EmailTemplate. At this point, our MongoDB database has two collections: users and sessions. We defined our User model in book/6-end/api/server/models/User.ts, and that is why we have a users collection. The express-session package creates a sessions collection without us explicitly defining a schema and model for Session.

No need for us to reinvent the wheel. Let's open our book/6-begin/api/server/models/User.ts file and visually ignore the part that defines static methods and types for static methods:

import * as mongoose from 'mongoose';

const mongoSchema = new mongoose.Schema({
    slug: {
        type: String,
        required: true,
        unique: true,
    },
    createdAt: {
        type: Date,
        required: true,
    },
    email: {
        type: String,
        required: true,
        unique: true,
    },
    displayName: String,
    avatarUrl: String,
    googleId: {
        type: String,
        unique: true,
        sparse: true,
    },
    googleToken: {
        accessToken: String,
        refreshToken: String,
    },
    isSignedupViaGoogle: {
        type: Boolean,
        required: true,
        default: false,
    },
});

export interface UserDocument extends mongoose.Document {
    slug: string;
    createdAt: Date;
    email: string;
    displayName: string;
    avatarUrl: string;
}

// static methods

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

export default User;

As you can see from above, besides defining static methods and types, we:
- defined mongoSchema
- defined and exported UserDocument document
- defined and exported User model

Since our EmailTemplate model will have no static methods, there is no reason for us to define EmailTemplateClass and there is no reason for us to call:

mongoSchema.loadClass(EmailTemplateClass);

So let's only achieve the following three goals for our EmailTemplate model:
- define mongoSchema
- define and export EmailTemplateDocument document
- define and export EmailTemplate model

Create a new file book/6-begin/api/server/models/EmailTemplate.ts and add the following content to it:

import * as _ from 'lodash';
import * as mongoose from 'mongoose';

const mongoSchema = new mongoose.Schema({
    name: {
        type: String,
        required: true,
        unique: true,
    },
    subject: {
        type: String,
        required: true,
    },
    message: {
        type: String,
    required: true,
    },
});

interface EmailTemplateDocument extends mongoose.Document {
    name: string;
    subject: string;
    message: string;
}

const EmailTemplate = mongoose.model<EmailTemplateDocument>('EmailTemplate', mongoSchema);

The EmailTemplate document in our database and the corresponding JS object on our API server will have only three parameters, all of them with the string data type and a unique name parameter.

In this book, we hardcode email templates into the code. When you start your API server (either locally or in production), our goal is to make the API server insert these hardcoded templates into our MongoDB database. We need to define a method to do this. Here is how we define an insertTemplates method:

async function insertTemplates() {
    const templates = [
        {
            name: 'welcome',
            subject: 'Welcome to SaaS by Async',
            message: `<%= userName %>,
                <p>
                    Thanks for signing up on our <a href="https://github.com/async-labs/saas" target="blank">SaaS boilerplate</a>!
                </p>
                <p>
                    If you are learning how to build a SaaS web app, check out our two books:
                    <a href="https://builderbook.org" target="blank">Builder Book</a>
                    and
                    <a href="https://builderbook.org/book" target="blank">SaaS Boilerplate</a>.
                </p>
                <p>
                    Also check out
                    <a href="https://async-await.com" target="blank"> Async</a>
                    , our communication tool for small teams of software developers.
                </p>
                Kelly & Timur, Team Async
            `,
        },
    ];

    for (const t of templates) {
        const et = await EmailTemplate.findOne({ name: t.name });

        const message = t.message
            .replace(/\n/g, '')
            .replace(/[ ]+/g, ' ')
            .trim();

        if (!et) {
            EmailTemplate.create(Object.assign({}, t, { message }));
        } else if (et.subject !== t.subject || et.message !== message) {
            EmailTemplate.updateOne({ _id: et._id }, { $set: { message, subject: t.subject } }).exec();
        }
    }
}

As you can see, templates is an array of objects. Currently, this array has only one object that corresponds to the welcome email. These objects from the templates array are used to create corresponding MongoDB documents in our database. Eventually, by the end of this book, you will have a total of four templates with the following names: welcome, login, invitation, and newPost.

You are already familiar with how if..else syntax works from Chapter 4 and Chapter 5. In the above code, we use if...else like this:

if (!et) {
    EmailTemplate.create(Object.assign({}, t, { message }));
} else if (et.subject !== t.subject || et.message !== message) {
    EmailTemplate.updateOne({ _id: et._id }, { $set: { message, subject: t.subject } }).exec();
}

If our API server does not find an EmailTemplate document in our database by name, then the API server will create a new MongoDB document using Mongoose's API method Model.create. We already discussed this method in Chapter 5 when we used it to create a new User document inside the static method signInOrSignUpViaGoogle (check up book/6-begin/api/server/models/User.ts file).

But if our API server does find an EmailTemplate document by name, then the API server will update the document's message and subject providing they have different values from the ones defined inside the templates array.

You are familiar with if..else syntax, but we have not used for...of syntax in this book:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for...of

If iterable is array, then for...of works like this:

const iterable = [10, 20, 30];

for (const value of iterable) {
    console.log(value);
}
// 10
// 20
// 30

console.log(value) runs multiple times. In the above example, it runs three times. In our case, it will run as many times as the number of email templates. We will ultimately have four email templates in our boilerplate, meaning console.log(value) will run four times. By the end of this section, it will only run one time, since we will only have the single welcome email template inside templates.


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 $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 August 16, 2020.

Last week to pre-order for $99. The price becomes $199 at midnight on Sunday, August 16th.


format_list_bulleted
help_outline
lens