Creating a Weekly Mailer with Rio
Denizhan Toprak
Posted on October 24, 2022
Project git: https://github.com/rettersoft/rio-samples
Rio Docs: https://docs.retter.io
What to Learn With This Project?
In this Rio project, our main goal is to creating a backend for an application that mails to its subscribers weekly.
In that road we are going to look at Rio's:
- Scheduling.
- Using other SDK's (Postmark).
- Communication between classes.
- Singleton architecture
- Model Usage
For following along you may want to use Postmark also. To getting your POSTMARK_API_TOKEN visit Postmark.
Our Project's End Result
First we need to have a logic for our subscribers. For example they need to be able to subscribe :). Like This:
After we run this method we need to validate the mail. So we e-mail the subscriber. Only then we will be able to send weekly mails to them. Like This:
After that we can send our weekly mails to validated subs!
Don't worry your mail doesn't need to be so simple. You can use the HTML as you please in your mails.
Let's get to it!
Creating Models
In Rio Models
are used to validate method's input, make them more readable and easier to Debug. For More: Rio Models.
We need only one model, which is Subscriber
. And only the email
will be mandatory. Rest will not be the input validation but the variables we need for further use.
Subscriber
{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email"
},
"isApproved": {
"type": "boolean"
},
"createdAt": {
"type": "number"
}
},
"required": ["email"],
"additionalProperties": false
}
Using Other SDK's
Using other SDK's are extremely easy in Rio. Just go to your class package.json and add your desired SDK and it's version. Mine is looking like this:
Than deploy your project. That' s it you can use your SDK in your class.
Note that we are going to use POSTMARK both Subscriber and Mailer class. So add postmark to both of them.
In postmark and many other SDK's we need a token to use the accual SDK functions. And adding the tokens manually in the files can be a little bit cumbersome
and unsafe (tokens may be precious). So adding to them to the Enviroments is accually a good idea. So lets do that.
In our files we can use this token like this:
const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN);
Subscriber Class
This class will be responsible of creating subscribers, e-mailing them for validation, unsubscribe them if wanted, and validate them. But first we need to kinda "initialize" the class in index.ts
. Let's go step by step.
template.yml
This file determines which methods are going to be used in this class and how to use them. For example we just created our model. In template.yml we decide which of the methods are going to use this model.
Our's looks like this for the Subscriber class:
init: index.init
getState: index.getState
getInstanceId: index.getInstanceId
methods:
- method: getSubscribers
handler: subscribe.getSubscribers
- method: subscribe
inputModel: Subscriber
handler: subscribe.subscribe
- method: validate
inputModel: Subscriber
handler: subscribe.validate
- method: unSubscribe
inputModel: Subscriber
handler: subscribe.unSubscribe
- method: sendMailToSubscribers
handler: subscribe.sendMailToSubscribers
As you see we give each method a name in method
, where to run them in handler
and if necessary an inputModel
for the models it is going to use.
There are a few more options if you want to check out in template docs.
index.ts
I am just going to drop the whole index.ts
then break it down.
import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";
const rdk = new RDK();
export async function authorizer(data: Data): Promise<Response> {
return { statusCode: 401 };
}
export async function init(data: Data): Promise<InitResponse> {
return {
state: {
private: {
subscribers: [],
preSubscribers: [],
},
},
};
}
export async function getInstanceId(data: Data): Promise<string> {
return "defaultInstance";
}
export async function getState(data: Data): Promise<Response> {
return { statusCode: 200, body: data.state };
}
In Init we determine the variables to be created when the class gets created. So we create a subscribers array which will hold the validated subscribers and the preSubscribers which are not validated.
We create them here so we don' t need to worry about "If they exist" later.
getInstanceId is a makes this whole project singleton. We return the instanceId as "defaultInstance" so there will be only one instance in this project which is the "defaultInstance". You can change it's return value to your needs of course.
Rest of the code is default code actually. So if you get curious about rest check the docs!
Subscribe.ts
This file going to hold the main methods we are going to use.
The imports and const values for this class looks likes this:
import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";
import { Subscriber } from "./rio";
const postmark = require("postmark");
const rdk = new RDK();
const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN); // you put your postmarkToken in Settings -> Enviroment (c.retter.io)
Let's keep going!
subscribe method
This method's input section needs to be looked at. If we look at this sample everything going to make sense:
export interface Data<I = any, O = any, PUB = KeyValue, PRIV = KeyValue, USER = UserState, ROLE = RoleState>
So for I
(input) we typed Subscriber
. So our Input value should be in the format of Subscriber (our model). And for PRIV (private state) we typed { preSubscribers: Subscriber[]; subscribers: Subscriber[]
. So in private state there should be preSubscribers and subscribers array. And their members should be in the format of Subscriber. That's it!
/**
* @description creates a preSubscriber and sends a confirmation email
*/
export async function subscribe(
data: Data<
Subscriber,
any,
any,
{ preSubscribers: Subscriber[]; subscribers: Subscriber[] }
>
): Promise<Data> {
// check if there is already a subscriber or presubscriber with the same email
let subscriber = data.state.private.preSubscribers.find(
(s) => s.email === data.request.body.email
);
if (!subscriber) {
subscriber = data.state.private.subscribers.find(
(s) => s.email === data.request.body.email
);
}
// if there is a subscriber with the same email
if (subscriber) {
data.response = {
statusCode: 404,
body: {
status: "This member is already registered to preSubscribers",
},
};
}
// if there is no subscriber with the same email
else {
const preSubscriber: Subscriber = {
email: data.request.body.email,
createdAt: Date.now(),
isApproved: false,
};
data.state.private.preSubscribers.push(preSubscriber);
// send mail to preSubscriber's mail to confirm subscription
await client.sendEmail({
From: "denizhan@rettermobile.com",
To: preSubscriber.email,
Subject: "Confirm your subscription",
HtmlBody: `
<h1>Welcome</h1>
<p>Thanks for trying Rio News. We’re thrilled to have you on board. To finish your register process, validate your account by clicking the link below:</p>
<!-- Action -->
<table class="body-action" align="center" width="100%" cellpadding="0" cellspacing="0">
<tr>
<td align="center">
<!-- Border based button https://litmus.com/blog/a-guide-to-bulletproof-buttons-in-email-design -->
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td>
<a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate</a>
</td>
</tr>
</table>
</td>
</tr>
</table>
</td>
</tr>
</table>
</table>
`,
//HtmlBody: `<html> <body> <h1>Confirm your subscription</h1> <p>Click <a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate</a> to confirm your subscription</p> </body> </html>`,
TextBody: `Confirm your subscription by clicking the link below \n https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}`,
});
data.response = {
statusCode: 200,
body: {
status: "New preSubscriber added to preSubscribers",
},
};
}
// send mail to new preSubscriber
return data;
}
This method basically gets the input e-mail from the request and check if there are any users which has the same e-mail. If not, it sends a mail to input e-mail address using postmark. And in validation link we run the validate
method.
How Did We Run a Method via Link?
If we take a closer look to this link:
<a href="https://${data.context.projectId}.api.retter.io/${data.context.projectId}/CALL/Subscribe/validate/defaultInstance?email=${preSubscriber.email}">Validate </a>
We should give the projectId
via ${data.context.projectId}
. This finds our project. Then we need to Call this method. After that we navigate to the method we want to run (in this case validate). Finally the instance value (in this case defaultInstance.
Giving parameters when running methods without tokens can be achieved by using querystringparams. In this example after we gave our instance we determine the variable email
and value will be what' s inside the brackets ${ }
.
Validate Method
As you can see we get the input using data.requst.queryStringParams
here. After that the variable we want (in this case email
).
/**
* @description validates a preSubscriber and adds it to subscribers and removes from preSubscribers
*/
export async function validate(
data: Data<
Subscriber,
any,
any,
{ preSubscribers: Subscriber[]; subscribers: Subscriber[] }
>
): Promise<Data> {
// check if user already registered to subscribers
let subscriber = data.state.private.preSubscribers.find(
(s) => s.email === data.request.queryStringParams.email
);
if (!subscriber) {
data.response = {
statusCode: 404,
body: {
status: "This member is not in the preSubscribers",
},
};
} else {
data.state.private.preSubscribers =
data.state.private.preSubscribers.filter(
(s) => s.email !== data.request.queryStringParams.email
);
subscriber.isApproved = true;
data.state.private.subscribers.push(subscriber);
data.response = {
statusCode: 200,
body: {
status: `New subscriber ${subscriber.email} added to subscribers`,
},
};
}
return data;
}
In this method we get the input email and check this email if it is in the preSubscribers
array which we hold in the state.private
. If nothing goes wrong. We create a subscriber from that email and add that subscriber to subscribers array.
unSubscribe Method
Nothing different here. This method is for the subsribers wants to leave our application.
/**
* @description Removes the subscriber to its email, from the preSubscribers and subscribers
*/
export async function unSubscribe(
data: Data<
Subscriber,
any,
any,
{ subscribers: Subscriber[]; preSubscribers: Subscriber[] }
>
): Promise<Data> {
// check if user already registered to subscribers
let subscriber = data.state.private.subscribers.find(
(s) => s.email === data.request.body.email
);
if (!subscriber) {
subscriber = data.state.private.preSubscribers.find(
(s) => s.email === data.request.body.email
);
}
if (!subscriber) {
data.response = {
statusCode: 404,
body: {
status: "This member is not in the subscribers or preSubscribers",
},
};
} else {
data.state.private.subscribers = data.state.private.subscribers.filter(
(s) => s.email !== data.request.body.email
);
data.state.private.preSubscribers =
data.state.private.preSubscribers.filter(
(s) => s.email !== data.request.body.email
);
data.response = {
statusCode: 200,
body: {
status: "Subscriber has removed from subscribers and presSubscribers",
},
};
}
return data;
}
We remove the all subscribers or preSubscribers which has the email given.
getSubscribers
Communcation Between Classes is a another aspect of this sample. This method provides that functionallity. We simply return the subscribers in this method to call this method from another class later.
/**
* @description returns the subscribers list for other classes to use
*/
export async function getSubscribers(
data: Data<any, any, any, { subscribers: Subscriber[] }>
): Promise<Data> {
data.response = {
statusCode: 200,
body: {
subscribers: data.state.private.subscribers,
},
};
return data;
}
Mailer Class
This class has only one purpouse: send weekly mail to the subcribers using scheduling.
template.yml
We determine when and how often the method is going to run in here.
Our template for this class looks like this:
init: index.init
getState: index.getState
getInstanceId: index.getInstanceId
methods:
- method: sendMailToSubscribers
type: STATIC
handler: sendMail.sendMailToSubscribers
schedule: cron(0 18 ? * MON-FRI *)
For scheduled methods, type must be Static. And we can determine the schedule ourselves. Rio uses cron
or we can use rate
like this: rate(30 minutes)
. For this example this sendMailToSubscribers
method is going to be run at 18.00 every weekday. You can customise that whatever you like. For further information about this check scheduling.
sendMailToSubscribers method and Whole File
Because this file only includes this method I tought it is best to share the whole picture.
import RDK, { Data, InitResponse, Response, StepResponse } from "@retter/rdk";
import { Subscriber, Classes } from "./rio"; // get the classes for calling the "getSubscribers" method we saw earlier
const postmark = require("postmark");
const rdk = new RDK();
const client = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN); // you put yours in Settings -> Enviroment (c.retter.io)
/**
* @description sends email to all subscribers
*/
export async function sendMailToSubscribers(
data: Data<any, any, any, { subscribers: Subscriber[] }>
): Promise<Data> {
const subscribeInstance = await Classes.Subscribe.getInstance();
const subscribers = await subscribeInstance.getSubscribers();
const subscribersArray = subscribers.body.subscribers;
//create mail template
const mailTemplate = {
From: "denizhan@rettermobile.com",
To: "",
Subject: "Hi From Retter!",
TextBody: "Hi there, here is your news this week: \n Blah Blah Blah",
};
// use await because mailing to all subscribers will not be happen instantly.
// otherwise errors may occure
await Promise.all(
subscribersArray.map(async (subscriber) => {
mailTemplate.To = subscriber.email;
await client.sendEmail(mailTemplate);
})
);
data.response = {
statusCode: 200,
body: {
status: "Mail sent to all subscribers",
subscribersArray,
},
};
return data;
}
As you see in the code, our first goal is to get subscribersArray. In order to do that we get the instance of Subscribe class we imported. Than we can call the other classes method from now on, such as getSubscribers
method from Subscribe class.
After getting the subscribers, we create our mail template for Postmark. We change the destination for these mails in a loop and send their recievers.
Note that we used await Promise.All
here. This is to making sure that every subscribers will get their mail.
All Done!!!
That' it. Now you can use this to mail your friends weekly? Or in a big project that has 100.000 subs? Who knows? Maybe completely different project is in your mind. Whatever it is, hope this text helped you!
If you have any suggestions for this article please share.
Thanks!
Posted on October 24, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.