Transactional Email Microservice with Zeebe and NestJS

By
  • Blog
  • >
  • Transactional Email Microservice with Zeebe and NestJS
TOPICS

30 Day Free Trial

Bring together legacy systems, RPA bots, microservices and more with Camunda

Sign Up for Camunda Content

Get the latest on Camunda features, events, top trends, and more.

TRENDING CONTENT

NestJS
NestJS

NestJS is a JavaScript Microservices framework for Node.js inspired by Angular. For some time now, front-end developers have been able to get the benefits of configuration by convention, dependency injection, and composition using decorators to build code bases whose structure can scale. Now it’s time for backend developers to get the same benefits. NestJS is a framework that clearly meets a need felt in the community – it was the fastest growing Node.js framework in 2018 (measured by GitHub stars).

NestJS leverages TypeScript, decorators, and the MVC architecture to enable developers to build applications that communicate over REST, WebSockets, gRPC, and GraphQL. With a new library from Dan Shapirnestjs-zeebe – you can now integrate Zeebe into a NestJS application.

First Impressions

I sat down to try my hand at NestJS with Zeebe. After some initial grappling, I found that – after I agreed to allow a framework to structure my code – I was rapidly productive, switching an existing Zeebe/REST project over to use NestJS in short order. I also found that much of the structure I had been manually creating in my codebases was enforced – and in fact provided by default – in NestJS, giving me a clean and consistent separation of concerns, and freedom to focus on the differentiating business value of my app. Bernd Rücker recently penned an article where he warns off undifferentiated heavy lifting (Camunda Cloud: The why, the what and the how); and NestJS is another example of this principle at play.

It’s what your API endpoints actually do, and how they work together that give your application business value – not (re-)writing API endpoint machinery (unless you are writing a framework). I enjoy the freedom of Node.js, and the ability and opportunity to do things from first principles in unique and innovative ways – and sometimes I just want to get things done in a way where I don’t have to decode the architecture when I revisit the code six moths later, or someone else takes over maintenance. With NestJS I can see that it does that heavy lifting for me, and the vibrancy of the community gives me confidence in investing in this particular way of doing it.

Building a Transactional Email Microservice

Transactional email is a common business task – business processes frequently involve sending some kind of email to users / customers: welcome emails, password resets, receipts, and so forth.

I built a transactional email service worker for Zeebe with NestJS. You can see the complete source code on GitHub.

Having a fairly generic service task / worker that can send an email to a user is extremely useful. I built this one to generate plain text / HTML emails from templates, selecting the template to use via a custom header on the service task, and templating in selected variables – such as name and email address – from the job.

Email Service Task

Actual sending of the email is accomplished using AWS SES (Simple Email Service) source, but you could just as easily use Mailgun, Sendgrid, or your own infrastructure.

If video is your thing, you can watch me build it from start-to-finish in real-time in these two videos (I streamed it live):

Part 1 Transactional Email NestJS Microservice in Zeebe.io

Part 2 Transactional Email NestJS Microservice in Zeebe.io

_Like, comment, subscribe!_

Starting a NestJS project is simple. Install the NestJS CLI and start a new project like this:

npm i -g @nestjs/cli
nest new project-name

Then, you can head on over to Dan’s README, and just copy and paste the code for the module, controller, and main.ts. An important part of the application bootstrap is in main.ts:

const microservice = app.connectMicroservice({
    strategy: app.get(ZeebeServer),
});

await app.startAllMicroservicesAsync();

This creates a new ZBClient instance. The NestJS Zeebe transport uses the zeebe-node library under the hood (I wrote that!). The zeebe-node library is object-oriented, but it provides an imperative ergonomic surface (“create a new client“, “create a new worker“). With NestJS, you have declarative ergonomics: “my app uses Zeebe“, “this is a Zeebe task worker“.

When I asked Katie Ots the difference between functional and imperative programming, she told me:

With imperative programming you say how to do something. With functional programming, you say what you want done.

I walked away from that conversation thinking to myself: “But surely something, somewhere, has to tell the program how to do it?

In the case of NestJS, this is accomplished through decorators that annotate the code with the “undifferentiated heavy lifting” – provided by the NestJS framework and Dan Shapir – that should be done to accomplish what you desire.

Compare this classic zeebe-node code:

NestJS

You’re basically one step away from Assembly Language programming like this.

With this NestJS code:

NestJS

Totally the future. “Make it so!”

Mailgen and Micromustache

I used Mailgen for the email templates. In order to template in the variables from the job, the Mailgen templates should have templated variables in them.

So where the Mailgen example has:

Mailgen with static strings

The parts you want to template with job variables need to be templated like this (source code):

Template strings in Mailgen

Rather than binding variables to each of the fields in the Mailgen template, I generate a plaintext and HTML version of the email, with the template strings in them, then run the generated emails through micromustache, binding the job variables to the template (source code).

Generate email

Constraining Visibility with FetchVariables

You probably want to avoid accidentally leaking sensitive information from your jobs via template strings. The last thing you want to do is to start emailing arbitrary data from workflows to customers!

In this case, you can constrain your worker to only receive a white-listed subset of known variables for each job using fetchVariables. This way you can support a known set of values for your templates, and protect against leaking data from your business processes. The fetchVariables option takes an array of strings that are the variable names that will be provided to the worker by the broker for each job.

The worker / broker boundary is a network boundary, so your typechecking breaks down there. You are back in the 90s in this case, and writing arbitrary strings for fetchVariables. The zeebe-node library – and by extension the NestJS Zeebe transport – supports type parameters on the job handler signature, to allow you to define a shape for both the job variables and the custom headers.

So you can do this:

Parameterised Types

There is no run-time validation, however – this is a design-time safety convenience only.

NestJS has support for design and run-time validation via DTOs – another example of the undifferentiated heavy-lifting it provides; and I’d like to see this supported in the Zeebe Nest transport for this scenario.

Summary

The entire NestJS project is just 196 lines of code, with comments (not including the email template). That’s not a lot of code to maintain, and pretty good for a transactional email microservice that can be (re-)used anywhere in your business processes. You can spin up multiple instances of it in Docker to service high-volume scenarios.

Using NestJS takes care of a lot of the undifferentiated heavy lifting, and allows you to focus on the business value part of your code. In that sense, it’s a good match for Zeebe, and Camunda Cloud, which also do a lot of undifferentiated heavy lifting.

I’m a fan.

A huge shout out to Dan Shapir for creating the NestJS Zeebe Transport!

Camunda Developer Community

Join Camunda’s global community of developers sharing code, advice, and meaningful experiences

Try All Features of Camunda

Related Content

We're streamlining Camunda product APIs, working towards a single REST API for many components, simplifying the learning curve and making installation easier.
Learn about our approach to migration from Camunda 7 to Camunda 8, and how we can help you achieve it as quickly and effectively as possible.
We've been working hard to reduce the job activation latency in Zeebe. Read on to take a peek under the hood at how we went about it and then verified success.