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
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
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:
Next, generate a content-type with the Strapi generate command below:
npm run strapi generate
We want this content type to have two fields: user and description. So your selection should look like the screenshot below:
Your codebase should look like this:
The above command will create an empty collection called Comments.
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 = {};
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.
Paste the following in the .env
file located in ./env
:
TWILIO_ACCOUNT_SID = AC82a29b91a67xxxxxxxxx
TWILIO_AUTH_TOKEN = 81682479468249xxxxxxxxxxx
MYNUM = +23490xxxxxxx
TWILIONUM = +16463xxxxxx
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));
},
};
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);
},
}
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: [],
},
},
],
};
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
:
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"}
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
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
Creating the Product Collection
Let's create another collection for our product API with the generate command.
npm run strapi generate
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;
},
};
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);
},
};
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: [],
},
},
],
};
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);
},
};
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.
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
}
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"
}
You should also get a notification on your email like below if everything goes successfully:
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
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
The above command will set up our React application, and it will start on http://localhost:3000/.
Next, open the codebase in any code editor of your choice. I will be using VSCode for this example:
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;
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 ➤
</button>
</div>
</form>
</React.Fragment>
);
}
}
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">
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>
);
}
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>
);
}
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;
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.
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.