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

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.


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


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

We will cover the following topics in this chapter:

  • Stripe API - API project
    - Stripe setup - API
    - Stripe methods - API
    - Stripe checkout callback - API
    - Stripe webhook - API
    - Team Model - API
    - User Model - API
    - Team Leader Express routes - API

  • Stripe API - APP project
    - Team Leader API methods - APP
    - Team data store - APP
    - User data store - APP
    - Billing page and testing - APP
    - TeamSettings page and testing - APP

  • Setup at Stripe dashboard and environmental variables
    - Testing Stripe API

  • Email notification for new post API - API project
    - Discussion and EmailTemplate Models - API
    - Express routes for adding and editing Discussion - API
    - sendDataToLambdaApiMethod API method - APP
    - Discussion data store - APP
    - CreateDiscussionForm, EditDiscussionForm and PostForm - APP
    - Testing updates to internal API infrastructure

  • Amazon API Gateway and AWS Lambda
    - Amazon API Gateway
    - AWS Lambda
    - Testing entire Email notification for new post API


Stripe API - API project link

In this section, we will discuss subscriptions and how to implement them using Stripe API. We will outline the entire Stripe API infrastructure for our application. Any SaaS product has to have a way to charge customers. Most non-enterprise SaaS businesses charge customers on a monthly basis, somewhere in the $25-$500 range. In this book, we will show you how to implement the following features in our SaaS boilerplate, so that a Team Leader end user can:
- buy a subscription (subscribe to a monthly plan)
- unsubscribe (stop a subscription)
- update card information
- see a list of all invoices for subscription payments

In your real world SaaS product, you are the decision maker for what features to provide to your customers in terms of billing. In this section, we will build a simple Billing page that allows an end user to do the above list of actions:
Builder Book

Besides figuring out what billing features to add to your SaaS product, you should decide when to ask customers to buy a monthly plan. Your particular SaaS product may offer a trial period or require all customers to become paying customers right away. For a trial period, you should decide whether you want to collect card information and whether customers are automatically charged at the end of the trial period. By knowing your customers well, you can decide what works best for them and for your business.

In this book, we are showing you a less aggressive approach of offering customers to pay for your SaaS product. We let any end user use our boilerplate, but we set a limit to the number of team members that a Team Leader can invite to their team. By this point in the book, you have two accounts, one for a Team Leader and one for a Team Member. We will set up a limit of two team members including the Team Leader. In other words, the Team Leader has to become a paying customer in order to add a third team member to team:
Builder Book

Team Leader Potato has to go to the Billing page and buy a subscription in order to add a third member to the Team Builder Book team.

We will be using a newer Stripe API that prescribes using Stripe Checkout, which redirects a potential customer to a Stripe-hosted checkout page to add payment information and confirm payment. This newer API also prescribes creating a Stripe session object that contains information about the customer, subscription, and redirect URLs for different outcomes, among other data. There are three possible outcomes with different redirect URLs. A Team Leader in our application will click the Buy Subscription button and be redirected to Stripe's Checkout Website. Then, this end user can either cancel or proceed with payment. If the end user cancels, he/she gets redirected to the Billing page and sees a cancel message. If the end user proceeds and an error is caught, he/she gets redirected back to the Billing page and sees an error message. Finally, if the end user proceeds and payment is successful, he/she gets redirected to the Billing page and sees an updated UI that shows (1) that this end user is indeed a paying customer, (2) that our application has saved this end user's card information, and (2) a list of invoices with a first invoice.

We can summarize Stripe API infrastructure in our application like this:
Builder Book


Stripe setup - API link

As we mentioned a bit earlier, we will be using the most recent Stripe API approach as of August 2020. The older approach was using a Stripe popup within your application. The newer approach prescribes us to use a checkout page hosted by Stripe, Stripe Checkout:
https://stripe.com/docs/payments/checkout

Checkout is a checkout page that works across devices, including mobile browsers. Our application has to redirect a potential customer to Checkout and then back to our application.

For every attempt to manually pay in our application, Stripe prescribes creating and retrieving a Session object:
https://stripe.com/docs/api/checkout/sessions

Stripe Session should be created for single payments and first subscription payments. In our case, we use it for first subscription payments. We need to create a Session object on our API server, then pass the Session's id, sessionId, to our APP project on the browser and call redirectToCheckout({ sessionId }) to redirect an end user from our Billing page to Stripe's Checkout page.
https://stripe.com/docs/js/checkout/redirect_to_checkout

When our API server gets a request from Stripe's server - API has to run some code to update the corresponding Team and User MongoDB documents. For example, we want to set the field isSubscriptionActive to true for the Team document and save a stripeCard object as a field to the User document. How do we find the correct Team and User to update? We retrieve a Session object that contains all necessary information to find the correct Team and User documents.

Here is a visual explanation of when we need to create and retrieve a Session. We need to create a Session before redirecting to Checkout. We need to retrieve a Session after getting a request from Stripe's server:
Builder Book

Go to Stripe's API documentation and find the API method for creating a Session:
https://stripe.com/docs/api/checkout/sessions/create

Select Node.js in the Select library dropdown:
Builder Book

As you can see, the Stripe API method we are looking for is stripe.checkout.sessions.create.

Go to Stripe's API documentation and find the API method to retrieve a Session:
https://stripe.com/docs/api/checkout/sessions/retrieve

Builder Book

The Stripe API method we are looking for is stripe.checkout.sessions.retrieve.

Let's define createSession on our API server. We, as developers, can specify the shape of a Session object; however, some properties are required. For example, payment_method_types, success_url, and cancel_url are required properties of any Session object. We want to save userId and teamId to be able to find User and Team documents later and update them.

createSession Stripe method:

const dev = process.env.NODE_ENV !== 'production';

const stripeInstance = new Stripe(
    dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
    { apiVersion: '2020-03-02' },
);

function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
    const params: Stripe.Checkout.SessionCreateParams = {
        customer_email: customerId ? undefined : userEmail,
        customer: customerId,
        payment_method_types: ['card'],
        mode,
        success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
        cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing?         redirectMessage=Checkout%20canceled`,
        metadata: { userId, teamId },
    };

    if (mode === 'subscription') {
        params.line_items = [
            {
                price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
                quantity: 1,
            },
        ];
    } else if (mode === 'setup') {
        if (!customerId || !subscriptionId) {
            throw new Error('customerId and subscriptionId required');
        }

        params.setup_intent_data = {
            metadata: { customer_id: customerId, subscription_id: subscriptionId },
        };
    }

    return stripeInstance.checkout.sessions.create(params);
}

Once we defined params, we called the Stripe API method stripeInstance.checkout.sessions.create(params). We should discuss a few parts of this method in more detail:
- The type of params is Stripe.Checkout.SessionCreateParams. You should have the stripe package already installed, so Stripe.Checkout.SessionCreateParams is available.
- success_url has an API endpoint, meaning we have to define a matching Express route, /stripe/checkout-completed/:sessionId. Then we access sessionId as req.params.sessionId and retrieve a Session object, update Team and User documents, and send res.redirect to the browser to redirect an end user from Checkout back to our application - either the Billing page with success UI or the Billing page with an error message via the redirectMessage prop and notify.
- cancel_url is straightforward to understand. req.query.redirectMessage is accessed by our code inside App.getInitialProps and populated as the redirectMessage prop in all pages. We need to call notify on the Billing page to show a cancel message to an end user.
- mode can have two values in our application. For making the first subscription payment and becoming a subscriber, the value for mode is subscription. For updating card information, the value of mode is setup: https://stripe.com/docs/payments/checkout/subscriptions/update-payment-details#create-checkout-session.
- When the mode value is setup, we have to check for customerId and subscriptionId to make sure a Stripe customer exists and a Stripe Subscription exists. Otherwise, it makes no sense to let a user edit their payment method.

The Stripe method retrieveSession takes one argument and expands properties of the Session object it retrieves with the Stripe API method stripeInstance.checkout.sessions.retrieve:

function retrieveSession({ sessionId }: { sessionId: string }) {
    return stripeInstance.checkout.sessions.retrieve(sessionId, {
        expand: [
            'setup_intent',
            'setup_intent.payment_method',
            'customer',
            'subscription',
            'subscription.default_payment_method',
        ],
    });
}

Expanding a response in Stripe API means getting additional data - in this case, an object's properties:
https://stripe.com/docs/api/expanding_objects

Builder Book

Place the above code into a new file, book/9-begin/api/server/stripe.ts. At the top of the file, add imports and a line that disables the camelcase rule (for example, the success_url parameter is not camelcase):

/* eslint-disable @typescript-eslint/camelcase */

import * as bodyParser from 'body-parser';
import Stripe from 'stripe';

import Team from './models/Team';
import User from './models/User';

const dev = process.env.NODE_ENV !== 'production';

const stripeInstance = new Stripe(
    dev ? process.env.STRIPE_TEST_SECRETKEY : process.env.STRIPE_LIVE_SECRETKEY,
    { apiVersion: '2020-03-02' },
);

function createSession({ userId, teamId, teamSlug, customerId, subscriptionId, userEmail, mode }) {
    const params: Stripe.Checkout.SessionCreateParams = {
        customer_email: customerId ? undefined : userEmail,
        customer: customerId,
        payment_method_types: ['card'],
        mode,
        success_url: `${process.env.URL_API}/stripe/checkout-completed/{CHECKOUT_SESSION_ID}`,
        cancel_url: `${process.env.URL_APP}/team/${teamSlug}/billing?redirectMessage=Checkout%20canceled`,
        metadata: { userId, teamId },
    };

    if (mode === 'subscription') {
        params.line_items = [
            {
                price: dev ? process.env.STRIPE_TEST_PRICEID : process.env.STRIPE_LIVE_PRICEID,
                quantity: 1,
            },
        ];
    } else if (mode === 'setup') {
        if (!customerId || !subscriptionId) {
            throw new Error('customerId and subscriptionId required');
        }

        params.setup_intent_data = {
            metadata: { customer_id: customerId, subscription_id: subscriptionId },
        };
    }

    return stripeInstance.checkout.sessions.create(params);
}

function retrieveSession({ sessionId }: { sessionId: string }) {
    return stripeInstance.checkout.sessions.retrieve(sessionId, {
        expand: [
            'setup_intent',
            'setup_intent.payment_method',
            'customer',
            'subscription',
            'subscription.default_payment_method',
        ],
    });
}

Stripe methods - API link

In this subsection, we will define four Stripe methods:
- The updateCustomer and updateSubscription Stripe methods will be used inside the Express route /stripe/checkout-completed/:sessionId, which we have yet to define.
- The cancelSubscription Stripe method will be used inside the static methods cancelSubscription and cancelSubscriptionAfterFailedPayment of the Team model.
- The getListOfInvoices Stripe method will be used inside the static method getListOfInvoicesForCustomer of the User model.

In the previous subsection, you familiarized yourself with Stripe API documentation. Inside the Express route /stripe/checkout-completed/:sessionId, we want to use the updateCustomer and updateSubscription Stripe methods to update Stripe Customer and Stripe Subscription default_payment_method parameter. After checking Stripe's API docs:
https://stripe.com/docs/api/customers/update
https://stripe.com/docs/api/subscriptions/update


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

We regularly update the codebase with recent syntax and stable versions for libraries. The latest update was May 2021.