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 8: Discussion API. Post API. Websockets.

The price will become $249 at midnight of December 31, 2020.


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


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

We will cover the following topics in this chapter:

  • Discussion API
    - Model and static methods - Discussion
    - Express routes - Discussion
    - API methods - Discussion
    - Data store - Discussion
    - Updatingn Team data store - Discussion
    - Discussion page
    - Discussion-specific components
    - Common components for Discussion API
    - Testing Discussion API without POST API

  • Post API
    - Model and static methods - Post
    - Express routes - Post
    - API methods - Post
    - Data store and store methods - Post
    - Updating DiscussionPageComp page and CreateDiscussionForm
    - Post-specific components
    - Testing Post API

  • Web sockets for Discussion
    - API server
    - APP client
    - Testing sockets


link Discussion API

In Chapter 7, we wrote code that redirects an end user to the YourSettings page after accepting aninvitation to join a team. We mentioned that later on we will change the redirect destination, since it makes little sense. As we keep working on our SaaS boilerplate, the question remains - what do end users do with our application? You, as a software developer and business owner, should answer this question early. What problem does your SaaS product solve? Who will pay for your product to solve this problem?

Since our SaaS boilerplate is a "boilerplate" after all, we will build a simple feature: Discussions. End users will discuss anything they want by creating a Discussion. A Discussion has participants and consists of Posts. Any member of a team (Team Leader or Team Member) will be able to create a new Discussion, name that Discussion, select participants from the team's members, and create a very first Post inside this new Discussion.

Builder Book

At the end of this section, we will redirect an end user who accepted a team invitation to the Discussion page instead of the YourSettings page. The Discussion page that we build in this chapter, after we are done with Post-related components, will look like this:

Builder Book

Note that the page's route contains /team/:teamSlug/discussions/:discussionSlug, similar to the TeamSettings page. Recall how we wrote our Express route for the TeamSettings page:

server.get('/team/:teamSlug/team-settings', (req, res) => { const { teamSlug } = req.params; app.render(req, res, '/team-settings', { teamSlug }); });

We write a similar Express route for the Discussion page. We need to remember to add this route to our APP server:

server.get('/team/:teamSlug/discussions/:discussionSlug', (req, res) => {
    const { teamSlug, discussionSlug } = req.params;
    app.render(req, res, '/discussion', { teamSlug, discussionSlug });
});

Here is a highlight of most (but not all) components we will build for our Discussion API:

Builder Book

Builder Book

No changes to the API infrastructure - it's the same as the previous internal API infrastructures (for example, Team):

Builder Book


link Model and static methods - Discussion

We start building Discussion API with our API project (server-only code). Previously, we built Team API starting with our API server and Invitation API starting with our APP server/client. It's really up to you where to start when building internal API infrastructures. We, as a small team that released and continuously develops our SaaS product, Async, typically start building internal APIs from server-only code (API project).

In this subsection, we will define a new data model called Discussion. This task should be easy to complete by this point in the book, since we alreay defined four other data models:
- User
- EmailTemplate
- Team
- Invitation

As with as any other data model in this book, we follow these steps when defining a new model using Mongoose:
- defining Schema
- defining interface for Document
- defining interface for Model
- defining static methods for class that extends Model
- exporting Document
- define and export Model using Schema with static methods and the above interfaces
- adding required imports, settings

Let's go through the above blueprint to define a new data model called Discussion.

  • Schema has a createdUserId property - a user id of the Discussion's creator. It also has memberIds - an array of user ids who are participants of the Discussion. The Discussion's creator selects users from all team members to become that Discussion's participants. Only participants can see the Discussion and create new Posts within it. Schema also has a teamId property, because users create Discussions within a specific Team. In other words, members of one Team cannot see Discussions of another Team. An end user will access the Discussion page using a URL that contains /team/:teamSlug/discussions/:discussionSlug. There could be two Discussions with the same discussionSlug, but they have to be in two different Teams. When an end user loads the Discussion page, we will show list of all Discussions within the Team using the teamId property. Below, we will define a static method, getList, that retrieves and returns a list of all Discussions for a given Team.

In addition to the above properties, there are common properties such as name, slug, and createdAt:

const mongoSchema = new mongoose.Schema({
    createdUserId: {
        type: String,
        required: true,
    },
    teamId: {
        type: String,
        required: true,
    },
    name: {
        type: String,
        required: true,
    },
    slug: {
        type: String,
        required: true,
    },
    memberIds: [String],
    createdAt: {
        type: Date,
        required: true,
        default: Date.now,
    },
});
  • interface Document is for defining data types for properties we specified in Schema:

    interface DiscussionDocument extends mongoose.Document {
      createdUserId: string;
      teamId: string;
      name: string;
      slug: string;
      memberIds: string[];
      createdAt: Date;
    }
  • interface Model is for defining data types for methods' arguments and return values. We will have five static methods: getList, add, edit, delete, and checkPermissionAndGetTeam:

    interface DiscussionModel extends mongoose.Model<DiscussionDocument> {
      getList({
          userId,
          teamId,
      }: {
          userId: string;
          teamId: string;
      }): Promise<{ discussions: DiscussionDocument[] }>;
    
      add({
          name,
          userId,
          teamId,
          memberIds,
      }: {
          name: string;
          userId: string;
          teamId: string;
          memberIds: string[];
      }): Promise<DiscussionDocument>;
    
      edit({
          userId,
          id,
          name,
          memberIds,
      }: {
          userId: string;
          id: string;
          name: string;
          memberIds: string[];
      }): Promise<DiscussionDocument>;
    
      delete({ userId, id }: { userId: string; id: string }): Promise<{ teamId: string }>;
    
      checkPermissionAndGetTeam({
          userId,
          teamId,
          memberIds,
      }: {
          userId: string;
          teamId: string;
          memberIds: string[];
      }): Promise<TeamDocument>;
    }
  • Static method getList uses the Mongoose API method find to find and return an array of Discussion objects that have a matching teamId property:

    public static async getList({ userId, teamId }) {
      await this.checkPermissionAndGetTeam({ userId, teamId });
    
      const filter: any = { teamId, memberIds: userId };
    
      const discussions: any[] = await this.find(filter).setOptions({ lean: true });
    
      return { discussions };
    }

Before calling find, we call checkPermissionAndGetTeam to check all necessary permissions. Discussion.checkPermissionAndGetTeam is very similar to User.checkPermissionAndGetTeam. Both are private methods - they are only accessible and used inside a corresponding Model. User.checkPermissionAndGetTeam finds a Team document and checks if a user is indeed part of that Team, then returns the found Team object. Discussion.checkPermissionAndGetTeam does the same things as User.checkPermissionAndGetTeam but also checks if a user is a participant of a Discussion:


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

The price will become $249 at midnight of December 31, 2020.


format_list_bulleted
help_outline
lens