4 min read

Tutorial - Creating an Email Microservice with Typescript and Dapr

Tutorial - Creating an Email Microservice with Typescript and Dapr

Once in a while a new project comes out that I am totally hyped about! Dapr is such a Project! In this article I would like to go in-depth on how we can quickly (< 10min!!!) create a Microservice that will send an email through a Microservice invocation mechanism!

Dapr Introduction

In my own words, Dapr takes away the burden of integrating all these pesky APIs and allows you to focus on what counts: your business logic. Next to that, Dapr also implements building blocks for Service-to-Service Invocation, State Management, Publish and Subscribe, Actors, Observability, Secret Management all while being extensible!

What will we create?

In this article I want to create an Email Microservice which should be able to do the following:

  • We should be able to call a send endpoint from another microservice
  • We should be able to provide subject and emailTo email
  • I want to utilize Twilio Sendgrid
💡 I am utilizing Sendgrid, but Dapr allows us to instantly swap it out for any other supported binding as described in the documentation..

Bootstrapping our project

For this project I will be utilizing Typescript, since this is not the scope of the article to set this up, I recommend reading my previous article - bootstrapping a Typescript Project on how you can set this up yourself in a few commands.

Creating an Email Microservice

Once our initial Typescript project is bootstrapped, add a components folder as well for our Dapr components to be stored in. The final directory structure will then look like this:

.
├── README.md
├── components  # Holds all our bindings for Dapr
├── dist        # Holds the compiled code
├── node_modules
├── package-lock.json
├── package.json
├── src         # Holds the source code
└── tsconfig.json

Initializing our Dapr Component

In the components folder we just created we keep all the .yaml descriptions for Dapr to know how to configure its components. For sendgrid we create the following file:

components/binding-sendgrid.yaml

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: binding-sendgrid # the name of the component
  namespace: default
spec:
  type: bindings.twilio.sendgrid
  version: v1
  metadata:
  - name: emailFrom
    value: "[email protected]" # optional 
  - name: apiKey
    value: "<YOUR_SENDGRID_KEY>" # required, this is your SendGrid key

Creating the Email Service

Once that is done, we need to create the email service itself. The way this works is:

  1. We install the Javascript SDK (npm i -S @roadwork/dapr-js-sdk)
  2. We initialize the Javascript SDK (let client = new Dapr(...))
  3. We listen on incoming requests from the Dapr Invoker (client.invoker.listen(...))

Therefore, we put the following in our index.ts file:

src/index.ts

import Dapr, { InvokerListenOptionsMethod, Req, Res } from "@roadwork/dapr-js-sdk";

// Dapr is running completely internal, so we connect to 127.0.0.1:3500
const client = new Dapr("127.0.0.1", 3500);

// Implements the onSend method for when the Dapr invocation happened
async function onSend(req: Req, res: Res) {
  const subject = req.body?.subject;
  const emailTo = req.body?.emailTo;

  if (!subject || !emailTo) {
    throw new Error(JSON.stringify({
      error: "Missing subject or emailTo fields"
    }));
  }

  await client.binding.send("binding-sendgrid", "<html><body>Hello World</body></html>", {
    emailTo,
    subject
  });

  console.log("Done Sending Email")

  return res.json({
    isDone: true
  })
}

// Initialize the SDK to listen on the POST /send endpoint
// Note: in the SDK this spins up a server that listens on a port. This port is defined through the DAPR_APP_PORT environment variable, so we need to open it when starting Dapr!
async function main() {
  client.invoker.listen("send", onSend, { method: InvokerListenOptionsMethod.POST })
}

main()
  .catch((e) => {
    console.error(e);
    process.exit();
  })

Now this is done, we can start our application utilizing the command below. This command might seem complex, but it's actually quite straightforward:

  • We start our application npm run start:dev
  • We ensure that Dapr waits on our application listening on its internal port, which is set in the SDK by the environment variable DAPR_APP_PORT so we configure this and set the Dapr --app-port 3001 as well
  • We want Dapr running on port 3500 for debugging purposes (we can send local requests to http://127.0.0.1:3500 as the dapr endpoint)
  • We define our components path as ./components/ where Dapr will look for the bindings (or other components such as PubSub)
DAPR_APP_PORT=3001 dapr run --app-id msvc-email --app-port 3001 --dapr-http-port 3500 --components-path ./components/ npm run start:dev

Testing the Email Microservice

Everything is in place now to run the code, but we should still create some piece of code to quickly test this. Well, wrong! In normal SDKs we would have to do this, but Dapr actually implemented that with the CLI!!

We thus install the Dapr CLI (see documentation) which we initialize with dapr init

💡 This initializes Dapr self-hosted on our container environment

Once that is done we can utilize the internal invoke command to trigger our application (a dream come through for testing 😉) so run the command below to trigger this:

dapr invoke --app-id msvc-email --method send --data '{"subject": "my-subject", "emailTo": "[email protected]"}'

Which will kick of the Sendgrid binding and send an email towards your inbox!

Conclusion

So what seemed complex actually wasn't! In just 40 lines of code (which we can simplify to ~3 if we are not counting validation, …) we created a Microservice that can be called and implements an email sending service. Next to that we have the added benefits that scale-out is trivial, monitoring is out of the box, SDKs can be utilized for the invocation and most importantly service discovery is done for us! Thus, not requiring a central repository.