Using SMS and Email Services in Strapi

Strapi - Aug 19 '22 - - Dev Community

This article explains how to create and use SMS and email services in Strapi.

Author: @codev206

Being in a restaurant, the first thing that gets presented to you is the menu. This action repeatedly happens whenever you go to a restaurant. In the same vein, so many events or activities get repeated in software engineering. Therefore, it is nice that we employ the DRY (Don't Repeat Yourself) concept all the time to make things easier.

Prerequisites

To follow this article, you’ll need:

  • A Twilio Account (for SMS service)
  • Node.js (Version 14 and later)
  • A Gmail Account (for emails)

Controllers in Strapi

These two concepts relate. Controllers are where actions are stored. These actions get triggered when a client requests a particular route defined in the code. Controllers are responsible for controlling the flow of any application that follows the MVC framework, including Strapi.

Services in Strapi

Services help you with the DRY principle, as they do what they even mean; they serve. These are reusable functions that simplify controllers' logic.

Whenever you create a new content type or model, Strapi generates a new service file that does nothing but could override the generic service created in the node_module.

Let's spin up a project by creating a new application if you don't have one already.

    npx create-strapi-app@latest my-project --quickstart
    //Or
    yarn create strapi-app my-project --quickstart
Enter fullscreen mode Exit fullscreen mode

After installation, navigate to http://localhost:1337/admin and complete the form to create the first Administrator user.

Creating Services

First, we will create an API with its configurations, controller, and service.

    npm run strapi generate
Enter fullscreen mode Exit fullscreen mode

Then do the following.

  • Select api as the generator.
  • Enter comment for the name.
  • This API is not for a plugin, select n.

Your selections should look like the screenshot below:

Creating Comment API

Next, generate a content-type with the Strapi generate command below:

    npm run strapi generate
Enter fullscreen mode Exit fullscreen mode

We want this content type to have two fields: user and description. So your selection should look like the screenshot below:

Creating Comment Content-type

Your codebase should look like this:

Comment Schema

The above command will create an empty collection called Comments.

Viewing Comment collection

We want to use the Service Strapi generated to send SMS when a user creates a new comment. However, we can achieve endless possibilities of functionalities with the Strapi Generated Services.

Using Services to Send SMS

Create a file called sms.js in the ./api/comment/services folder and add paste this code to it:

    'use strict';
    module.exports = {};
Enter fullscreen mode Exit fullscreen mode

We will send an SMS whenever a user creates a comment using Twilio. Let's install Twilio using the following command:

Copying Your Twilio Credentials

Log in to Your Twilio Account or create one if you don't already have it here. Now, copy out your ACCOUNT SID and AUTH TOKEN.

Getting Twilio Credentials

Paste the following in the .env file located in ./env:

TWILIO_ACCOUNT_SID = AC82a29b91a67xxxxxxxxx
TWILIO_AUTH_TOKEN = 81682479468249xxxxxxxxxxx
MYNUM = +23490xxxxxxx
TWILIONUM  = +16463xxxxxx
Enter fullscreen mode Exit fullscreen mode

Where AC82a29b91a67xxxxxxxxx is your exact ACCOUNT SID and 81682479468249xxxxxxxxxxx is the same AUTH TOKEN you copied from your Twilio account. TWILIONUM will be the exact phone number given by Twilio, and MYNUM should be the destination number.

Then we will create a function that will be exported and be globally accessible through the strapi.services.

In our service file at ./api/comment/services/sms.js:

    module.exports = {
      sendSms() {
        const accountSid = process.env.TWILIO_ACCOUNT_SID;
        const authToken = process.env.TWILIO_AUTH_TOKEN;
        const myNum = process.env.MYNUM;
        const twilioNum = process.env.TWILIONUM;
        const client = require("twilio")(accountSid, authToken);
        client.messages
          .create({
            body: "Hello Admin, someone just posted a comment",
            from: twilioNum, //the phone number provided by Twillio
            to: myNum, // your own phone number
          })
          .then((message) => console.log(message.sid));
      },
    };
Enter fullscreen mode Exit fullscreen mode

Triggering the SMS Services

Now let us go to the ./api/comment/controllers/comment.js and define what will happen whenever a user comments on our application.

In our ./api/comment/controllers/comment.js, we will call the global strapi.services and other methods we created in ./api/comment/services/sms.js.

    module.exports = {
        async create(ctx) {
        strapi.service("api::comment.sms").sendSms();
        return await strapi
          .service("api::comment.comment")
          .create(ctx.request.body);
        },

        async get(ctx) {
        return await strapi
          .service("api::comment.comment")
          .get(ctx.request.body);
      },
    }
Enter fullscreen mode Exit fullscreen mode

Whenever we make a post request in Comments collections, it calls the Customs Service, which communicates to the Twilio server and sends us an SMS. Now create the create service in ./api/comment/service/comment.js to save the actual comment to our collection.

    "use strict"
    module.exports = () => ({
      async create(data) {
        return await strapi.entityService.create("api::comment.comment", {
          data,
        });
      },

       async get() {
        return await strapi.entityService.findMany("api::comment.comment");
      },
    });

Finally, configure a route for our `create` service in `./api/comment/routes/comment.js` with the code snippet below:


    module.exports = {
      routes: [
        {
          method: "POST",
          path: "/comment",
          handler: "comment.create",
          config: {
            policies: [],
            middlewares: [],
          },
        },
        {
          method: "GET",
          path: "/comment",
          handler: "comment.get",
          config: {
            policies: [],
            middlewares: [],
          },
        },
      ],
    };
Enter fullscreen mode Exit fullscreen mode

Creating a New Comment with Postman

We can test whether or not the SMS will be delivered when we try to create a new comment by making a post request. Make sure you have the create access in your application role by navigating to Settings->USERS & PERMISSIONS PLUGIN->Roles-> Public:

Creating access permission

So we will be using Postman to send a POST request to this URL http://localhost:1337/comments. Fill in the following JSON data in the request body and hit the Send button.

    {"user": "Precious",
    "description": "I just want to comment that Strapi is dope"}
Enter fullscreen mode Exit fullscreen mode

testing with Postman

You should also receive the SMS delivered to your phone number.

Using Services to Send Emails

Next, we will talk about how to send emails using custom services. We will try to notify ourselves when a product is added to the collection. We should get notified through email.

Let's create a new API for that:

    npm run strapi generate
Enter fullscreen mode Exit fullscreen mode

Creating Emails API

The command will create a new folder in ./api/ called product with the following sub-folders routes, services controllers. We will use a package called nodemailer. So make sure you install it using the command below.

    npm install nodemailer
Enter fullscreen mode Exit fullscreen mode

Creating the Product Collection

Let's create another collection for our product API with the generate command.

    npm run strapi generate
Enter fullscreen mode Exit fullscreen mode

Creating Email Content-type

Now, paste the following codes in the service of our just created product found in ./api/product/services/product.js.

    const toEmail = process.env.TOEMAIL;
    const welcome = process.env.WELCOME;
    module.exports = {
      async create(data) {
        const response = await strapi.entityService.create("api::product.product", {
          data,
        });
        strapi
          .service("api::comment.sendmail")
          .send(
            welcome,
            toEmail,
            "Welcome",
            `A product has been created ${entity.name}`
          );
        return response;
      },
    };
Enter fullscreen mode Exit fullscreen mode

Next, create a controller for the create service in the ./api/product/controllers/product.js file with the code below:

    module.exports = {
      async create(ctx) {
        return await strapi
          .service("api::prooduct.prooduct")
          .create(ctx.request.body);
      },
    };
Enter fullscreen mode Exit fullscreen mode

Then configure the route in the ./api/product/routes/product.js file with the code below:

    module.exports = {
      routes: [
        {
         method: 'POST',
         path: '/product',
         handler: 'product.create',
         config: {
           policies: [],
           middlewares: [],
         },
        },
      ],
    };
Enter fullscreen mode Exit fullscreen mode

Ensure you have the create access in your application role in the product(Settings->USERS & PERMISSIONS PLUGIN->Roles-> Public). And of course, all your environment variable (TOEMAIL and WELCOME) are defined in the .env file.

Create asendmail.js file in ./api/sendmail/services/ and add the code below:

    const nodemailer = require('nodemailer');
    const userEmail = process.env.MYEMAIL
    const userPass = process.env.MYPASS
    // Create reusable transporter object using SMTP transport.
    const transporter = nodemailer.createTransport({
      service: 'Gmail',
      auth: {
        user: userEmail,
        pass: userPass,
      },
    });
    module.exports = {
      send: (from, to, subject, text) => {
        // Setup e-mail data.
        const options = {
          from,
          to,
          subject,
          text,
        };
        // Return a promise of the function that sends the email.
        return transporter.sendMail(options);

      },
    };
Enter fullscreen mode Exit fullscreen mode

Also, define all your environment variables (MYEMAIL and MYPASS) in the .env file.
This is your Gmail email address and the password to access it. Unfortunately, for our app to have access to our email, we need to lessen the security of Gmail a little bit. This is because Google does not let third-party applications access its accounts without approvals.

Go to your Google Accounts and put ON less secure app access.

Enabling Less secure app access

Next, create a controller for our product API's create service.
Now we will create a new product in Postman(HTTP Client). Send a Post request to this URL http://localhost:1337/products. Add the JSON data below to the request body:

{
"name": "A headphone",
"price": 2000
}
Enter fullscreen mode Exit fullscreen mode

Testing Email endpoint

When you hit the Send button, you should get this response below if everything goes successfully:

{
    "id": 5,
    "name": "A headphone",
    "price": 2000,
    "createdAt": "2022-05-05T12:23:09.965Z",
    "updatedAt": "2022-05-05T12:23:09.965Z"
}
Enter fullscreen mode Exit fullscreen mode

You should also get a notification on your email like below if everything goes successfully:

Confirming Email sent

This Email Notification task is just a tip of what you can achieve with Strapi Services. The use case of Services is limitless. You can do any business logic.

Building a Commenting App

Now, what's this whole concept of services in Strapi without an actual example of how it works? So I will be using Reactjs to show you one of the many ways services in Strapi work. Let's move away from our current Strapi project. Instead, we will create a new application with create-react-app.

In a different directory, run this command to create a new React app:

    npx create-react-app strapicomment
Enter fullscreen mode Exit fullscreen mode

I decided to call my application strapicomment ( you can call yours anything). After our react application has been created, let's move into its directory and start the application.

    cd strapicomment
    yarn start
Enter fullscreen mode Exit fullscreen mode

The above command will set up our React application, and it will start on http://localhost:3000/.

Starting React Project

Next, open the codebase in any code editor of your choice. I will be using VSCode for this example:

Project structure

Cleaning it up

We will clean up the project and remove some unnecessary codes with React Quickstart boilerplate. In the src folder, delete the logo.svg, and create a folder called components (which is where all our components will be going).

Next, copy and paste this code to replace the existing code in App.js:

    function App() {
      return (
        <div className="App">
          <h1>Hello React</h1>
        </div>
      );
    }
    export default App;
Enter fullscreen mode Exit fullscreen mode

Let’s create three components in .src/components namely Form.js, List.jsx, and Comment.jsx In our Form.js, paste in the following codes.

    import React, { Component } from "react";
    export default class Form extends Component {
      constructor(props) {
        super(props);
        this.state = {
          loading: false,
          error: "",
          comment: {
            user: "",
            description: ""
          }
        };
        // bind context to methods
        this.handleFieldChange = this.handleFieldChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
      }
      /**
       * Handle form input field changes & update the state
       */
      handleFieldChange = event => {
        const { value, name } = event.target;
        this.setState({
          ...this.state,
          comment: {
            ...this.state.comment,
            [name]: value
          }
        });
      };
      /**
       * Form submit handler
       */
      onSubmit(el) {
        // prevent default form submission
        el.preventDefault();
        if (!this.isFormValid()) {
          this.setState({ error: "All fields are required." });
          return;
        }
        // loading status and clear error
        this.setState({ error: "", loading: true });
        // persist the comments on server
        let { comment } = this.state;
        fetch("http://localhost:1337/api/comment", {
          headers:{'Content-type':'application/json'},
          method: "post",
          body: JSON.stringify(comment)
        })
          .then(res => res.json())
          .then(res => {
            if (res.error) {
              this.setState({ loading: false, error: res.error });
            } else {
              this.props.addComment(comment);

              this.setState({
                loading: false,
                comment: { ...comment, description: "" }
              });
            }
          })
          .catch(err => {
            this.setState({
              error: "yo! something is sideways",
              loading: false
            });
          });
      }
      /**
       * Simple validation
       */
      isFormValid() {
        return this.state.comment.user !== "" && this.state.comment.description !== "";
      }
      renderError() {
        return this.state.error ? (
          <div className="alert alert-danger">{this.state.error}</div>
        ) : null;
      }
      render() {
        return (
          <React.Fragment>
            <form method="post" onSubmit={this.onSubmit}>
              <div className="form-group">
                <input
                  onChange={this.handleFieldChange}
                  value={this.state.comment.user}
                  className="form-control"
                  placeholder="UserName"
                  name="user"
                  type="text"
                />
              </div>
              <div className="form-group">
                <textarea
                  onChange={this.handleFieldChange}
                  value={this.state.comment.description}
                  className="form-control"
                  placeholder="Your Comment"
                  name="description"
                  rows="5"
                />
              </div>
              {this.renderError()}
              <div className="form-group">
                <button disabled={this.state.loading} className="btn btn-primary">
                  Comment &#10148;
                </button>
              </div>
            </form>
          </React.Fragment>
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

I am using bootstrap for basic styling. I decided to bring it in through CDN, so go to the public folder in your root and locate index.html and paste this in between your head tags:

     <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.0/css/bootstrap.min.css" 
    integrity="sha384-9gVQ4dYFwwWSjIDZnLEWnxCjeSWFphJiwGPXr1jddIhOegiu1FwO5qRGvFXOdJZ4" 
    crossorigin="anonymous">
Enter fullscreen mode Exit fullscreen mode

In our List.jsx, paste in the following codes.

    import React from "react";
    import Comment from "./Comment";
    export default function List(props) {
      return (
        <div className="commentList">
          <h5 className="text-muted mb-4">
            <span className="badge badge-success">{props.comments.length}</span>{" "}
            Comment{props.comments.length > 0 ? "s" : ""}
          </h5>
          {props.comments.length === 0 && !props.loading ? (
            <div className="alert text-center alert-info">
              Be the first to comment
            </div>
          ) : null}
          {props.comments.map((comment, index) => (
            <Comment key={index} comment={comment} />
          ))}
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

What we are doing here is mapping through and displaying the available comments. If there is none, you will be the first to comment.
In our Comment.jsx, paste in the following codes.

    import React from "react";
    export default function Comment(props) {
      const { user, description } = props.comment;
      return (
        <div className="media mb-3">
          <div className="media-body p-2 shadow-sm rounded bg-light border">
            <h6 className="mt-0 mb-1 text-muted">{user}</h6>
            {description}
          </div>
        </div>
      );
    }
Enter fullscreen mode Exit fullscreen mode

Back to App.js in the src folder, replace it with the codes below.

    import React, { Component } from "react";
    import List from "./components/List";
    import Form from "./components/Form";
    class App extends Component {
      constructor(props) {
        super(props);
        this.state = {
          comments: [],
          loading: false
        };
        this.addComment = this.addComment.bind(this);
      }
      componentDidMount() {
        // loading
        this.setState({ loading: true });
        // get all the comments
        fetch("http://localhost:1337/api/comment")
          .then(res => res.json())
          .then(res => {
            this.setState({
              comments: res,
              loading: false
            });
          })
          .catch(err => {
            this.setState({ loading: false });
          });
      }

      addComment(comment) {
        this.setState({
          loading: false,
          comments: [comment, ...this.state.comments]
        });
      }
      render() {

        return (
          <div className="App container bg-light shadow">

            <div className="row">
              <div className="col-4  pt-3 border-right">
                <h6>Speak your Truth</h6>
                <Form addComment={this.addComment} />
              </div>
              <div className="col-8  pt-3 bg-white">
                <List
                  loading={this.state.loading}
                  comments={this.state.comments}
                />
              </div>
            </div>
          </div>
        );
      }
    }
    export default App;
Enter fullscreen mode Exit fullscreen mode

We have successfully created our application. Whenever a user comments, we get notified through SMS. We can do the same with email or any functionality across our minds.

Sample

Github Links

The code for both the React App and the Strapi backend is available here.

Conclusion

Strapi services offer a whole lot of benefits, and that makes developing easy. We have seen how this works in the little application that sends SMS using Twillio API whenever a user comments on our application. We have also seen how to create email notifications with Strapi Services.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .