App Unboxing
Build an email subscription app - Part 3: Deploy the backend
FEBRUARY 18, 2020 - 11 MINS READ

Overview

This tutorial is a 3-part series and will give a detailed walk through of how to do state management on the component level with Zustand. We'll show how to use Zustand in a tactically relevant way, while creating a fully integrated React component.

Here's a breakdown of what we'll be covering throughout the series:

As a caveat, each part above is linked to a Code Sandbox, complete with the section's fully completed code, for convenience. To make the most use of your time while following this tutorial, we recommend opening and forking the part's sandbox at the beginning of the section in a separate tab. Our Code Sandbox example can be your 'target repo'. While you're completing each part of the tutorial, your goal should be to write code that eventually resembles the target.

Prerequisites

This part has the same prerequisites as Part 1.

Our Objective

With this part, we're looking to connect a robust, scaleable backend to our Morning Brew replica and do it in less than 10-minutes.

Create a new Recipe in the editor

We're going to create one Recipe that handles the querying of dynamic content,  as well as storing the leads and sending an email. To do this, we're going to first head to the Recipe tab of your Buildable developer dashboard, create a new Recipe and give it a name.

Setting up the Trigger

In the trigger step, we're going to turn off the Recipe's authorization and add a two input parameters:

  • key  - a String, which is not-optional
  • email - an Email, which is optional

We'll also provide an example payload, which will look like this:



{
  "key": "content"
}



You're final trigger step will look like this once complete (keep in mind, your Endpoint URL will be different):

Add a Conditional step

Select the "+" symbol below the Trigger step and select the Conditional option from the step selector to add a Conditional route to your Recipe. For organization's sake, it's always a good idea to add a clear name for each step in your Recipe. In this case, we're going to call this Conditional step: isContent

Set up your condition

We're going to add conditional logic and branch whether this Recipe is going to return the content or expect a submission.

To start, let's add the rule for when the step should branch to the YES route. In other words, we want to route to the left side of the conditional when the following rule is met: $steps.trigger.key contains 'content'

Add conditional logic

In this YES branch, we're going to add a Function step and name the function "content". In the code snippet section, we're going to drop in the following code:



const content = {
  title: "Become smarter in just 6 minutes",
  subTitle:
    "You're now witnessing the power of a fully dynamic component 🤯",
  input: {
    id: "email-input",
    type: "email",
    label: "Enter your email",
    placeholder: "Enter your email",
    variant: "outlined"
  },
  errors: {
    invalidEmail: "We require a valid email",
    empty: "Email is required please"
  },
  button: {
    states: {
      initial: "Submit",
      processing: "Sending request",
      success: "Sent successfully",
      failed: "Failed! Try again."
    }
  }
};

const onRun = () => {
 
  return {
    ...content
  };

};



It's important to note that the content variable is the same as the content variable from our fallback.js file from our set up (See Part 2 for more information).

The Recipe now looks like this:

Now, we'll add a Response step to the YES branch. We're going to name our response step "content response". In this step we're going to leave the status code as a 200 OK. A response step ends the Recipe and returns to the client the body of the step. For more info on response steps, visit Buildable's Documentation on Responses.

We're also going to spread the data from the previous function step. To do this, we'll add a key value of __spread__ and select the $steps.is-content.pass.content.data as the value. An easy way to add this variable is to type content.data and click the available option that appears. For more information on how to properly pass data, visit Buildable's Documentation on How to Pass Data between Steps

Your Recipe should now look like this:

Let's confirm everything is working properly by testing the Recipe quickly.  To run the test, click the Test Recipe button on the top right side of the editor. When you run the test, check the trigger step to ensure you get the appropriate output. If you see the following output, your Recipe test is a success!

Create the Leads Service

Now that we've got the YES branch complete for the conditional, we'll start working on the NO branch. To start, we'll create a Leads service using your microservice generator. Head to the Services tab of the Buildable dashboard and create a Service, called Leads. This will be where we'll store the data for all users that enter their email in the Email Collection component.

Once the Leads service is created, it'll now be accessible in our Recipe editor. Head back to the Conditional step in your Recipe and do the following:

  • In the NO branch of your Conditional, add a Service step
  • In the first dropdown, select the Leads service
  • In the second dropdown, select the Create CRUD operation
  • Turn on the "Pause this action when running tests?" toggle

When you're done, the content of the drawer when you click the Service step in your Recipe should look like this: 

At this point, if this branch route is engaged an empty Lead record will be created. Moving forward, we don't want empty lead records, so let's ensure all Lead records are complete with useful data by applying an example payload in trigger. We cover this below.

Change the payload in the Trigger step to simulate a user submission

We want to execute the NO branch, which will be used when a user submits an email in our app. When an email is added in the field and the submit button is clicked, we want to capture the user email and store it in our Leads Service.

To start, head back to the Trigger step. From there, adjust the payload with the following:



{
  "key": "submission",
  "email": "lead@email.com"
}



Once you added the payload, test the Recipe again to ensure the NO branch is engaged as we'd expect. Click Test recipe in the top right corner of the editor and open the Conditional step result. You'll notice the executed branch is route with the NO label.

Once this is done, head back to the Service step and add the data you'd like to be included in the Create Lead CRUD action. In this case, it'll be

  • email - $steps.trigger.email
  • ip - $steps.recipeHeaders.x-forwarded-for
  • country - $steps.recipeHeaders.cf-ipcountry

When the NO branch is executed, the Recipe will create a Lead record with the email, ip and country. Keep in mind, this Recipe doesn't handle uniqueness of email. This can be tackled using the Recipe editor, but we'll skip it for simplicity's sake here.

Run a full test to create test data

To see everything in action, we're now going to call the Recipe with a submission and view the generated Lead record.

Start by first heading to the create-lead Service step and turning off the "Pause this action when running tests?" toggle. Run a test and return the pause toggle to its ON state.

Enabling the "Pause this action when running tests?" toggle is helpful whenever you're making multiple tests, while using Service steps. This'll help ensure you're not mistakenly creating more data then you intend.

From here, we should expect a Lead record to existing within our Leads service, so let's go check our service. Head back to the Services tab of the Buildable dashboard and click into the Leads service. You'll now notice a record exists with the data we passed.

Connect our Recipe to our component

We're going to use Axios to make an HTTP request from the component. Head to your React app and add a new file to the logic folder, called api.js. In this file, add the following code snippet:



import axios from "axios";

const POST = "POST";

const config = {
  recipeUrl: "https://api.buildable.dev/trigger",
  recipeEnv: process.env.NODE_ENV === "development" ? "test" : "live",
  version: "v2"
};

const { recipeUrl, recipeEnv, version } = config;

const api = async ({ payload, url, headers = {} }) => {
  const { data } = await axios({
    method: POST,
    url,
    headers,
    data: payload
  });

  return data;
};

const dispatchRecipe = ({ triggerId, payload = {}, options = {} }) =>
  api({
    ...options,
    url: `${recipeUrl}/${version}/${recipeEnv}-${triggerId}`,
    payload
  });

export default dispatchRecipe;



In the above code snippet, we've created a small wrapper around the Axios to make dispatching the Recipe easier. Now, head to the data-models.js file and import the dispatchRecipe from the api.js file. You'll also need to update the getContent method (inside the initContentModel) with the following snippet:



getContent: async () => {
    const payload = {
      key: "content"
    };
    const content = await dispatchRecipe({
      triggerId: RECIPES.LEADS_BLOCK,
      payload
    });
    set((state) => ({ ...state, content }));
  },



The complete updated file should look like the following:



import { content } from "../fallback";
import dispatchRecipe from "./api";

const RECIPES = {
  LEADS_BLOCK: "YOUR_RECIPE_TRIGGER_ID"
};

const initContentModel = (set) => ({
  content,
  currentButtonText: content.button?.states?.initial,
  setButtonText: (buttonText) =>
    set((state) => ({ ...state, currentButtonText: buttonText })),
  getContent: async () => {
    const payload = {
      key: "content"
    };
    const content = await dispatchRecipe({
      triggerId: RECIPES.LEADS_BLOCK,
      payload
    });
    set((state) => ({ ...state, content }));
  },
  setContent: (content) => {
    set((state) => ({ ...state, content }));
  }
});

const initLoadingModel = (set) => ({
  loading: false,
  processing: false,
  setLoading: () => {
    set((state) => ({ ...state, loading: true }));
  },
  clearLoading: () => {
    set((state) => ({ ...state, loading: false }));
  },
  setProcessing: () => {
    set((state) => ({ ...state, processing: true }));
  },
  clearProcessing: () => {
    set((state) => ({ ...state, processing: false }));
  }
});

export { initContentModel, initLoadingModel };


We'll then need to copy and paste your Recipe's trigger ID into the triggerId field. To find your trigger ID, simply click the Trigger step of the Recipe.

Once you've found the Recipe's triggerId, put it in the code snippet where you see "YOUR_RECIPE_TRIGGER_ID".

Create the getContent flow

Head to components/email-block/logic/flows.js and update the file to look something like this:



import { useStore } from "./store";
import { content as fallbackContent } from "../fallback";

const wait = async (time) =>
  new Promise((resolve) => setTimeout(() => resolve(true), time));

const useDispatchEmailFlow = () => {
  const [
    setProcessing,
    clearProcessing,
    setButtonText,
    buttonStates
  ] = useStore((store) => [
    store.setProcessing,
    store.clearProcessing,
    store.setButtonText,
    store.content?.button?.states
  ]);

  const dispatch = async () => {
    setProcessing();
    setButtonText(buttonStates?.processing);
    await wait(2000);
    setButtonText(buttonStates?.success);
    await wait(1000);
    setButtonText(buttonStates?.initial);
    clearProcessing();
  };
  return dispatch;
};

const useDispatchGetConentFlow = () => {
  const [
    setLoading,
    clearLoading,
    getContent,
    setContent
  ] = useStore((store) => [
    store.setLoading,
    store.clearLoading,
    store.getContent,
    store.setContent
  ]);

  const dispatch = async () => {
    setLoading();
    try {
      await getContent();
    } catch (error) {
      setContent(fallbackContent);
    }
    clearLoading();
  };
  return dispatch;
};

export { useDispatchEmailFlow, useDispatchGetConentFlow };



In this file, we're creating the useDispatchGetContentFlow that either:

  • gets the content dynamically from the Recipe
  • uses the fallback.js content if unable to pull dynamic content

Let's now request this data inside the component. In the EmailBlock.js file, we're going to import useDispatchGetContentFlow and call it inside a useEffect. Update the EmailBlock.js file to look like the following:


Conclusion

Congratulations! You've completed everything required to deploy your web app. In this part of the series, you learned how to build  the microservice architecture and backend logic for our entire web app. We used function, conditional and response steps to do so.

On your own time, you can also add a SendGrid email step to send an email to all users that submit their email. For information on how to connect email triggers, visit Buildable's SendGrid Integration documentation.

You can find the app’s code on this Code Sandbox.