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 6: AWS SES API. Passwordless OAuth API. Mailchimp API.

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 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.


AWS SES API link

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

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.


EmailTemplate model, insertTemplates and getEmailTemplate methods link

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-begin/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();
}

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

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