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:
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 thePOST method
from ourAPI
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.