Event-Driven Email Microservice with NestJS and Twilio SendGrid
Time to read:
Event-Driven Email Microservice with NestJS and Twilio SendGrid
A few years ago, when I moved from a smaller team to a larger one, I noticed everyone was obsessed with microservices, and I was even more surprised to see that this obsession extended to their email service structure. But it slowly started making sense to me when I realized how the basic flow I was used to quickly resulted in bottlenecks, especially when we had to add new email logic or maintain the codebase, because it was tied too closely to the core system. It became clear to me that for any system meant to scale and evolve rapidly, email services need to operate as a standalone, something like an event-driven layer.
In this post, you’ll learn how to build an email microservice using Twilio SendGrid and NestJS. You’ll map internal app events to SendGrid templates, trigger emails dynamically, and configure a webhook listener to track delivery, open, and bounce events in real time, all in a modular and scalable way.
Prerequisites
To complete the tutorial, you will need the following:
- A free Twilio SendGrid account
- Node.js Installation
- NestJs installed on your machine. If you don’t already have it installed, you can install it by doing this
npm i -g @nestjs/cli
or going to the documentation to learn how - Basic knowledge of Node.js and NestJS, and how to set up a NestJs Project
- A text editor like VS Code
- An understanding of how environment variables work.
- A verified sender email in your SendGrid account
- Ngrok installed for local webhook testing
Setting up the NestJs project
Creating the project folder and generating the NestJS app
To get started, create a folder where your email microservice will live. So, in your terminal, navigate to the directory where you want to keep your project and run, then run the commands below:
If you do not have NestJs installed on your device, you can make use of the command below to set it up. Make sure you have Node.js (version >= 20) is installed on your operating system
Now that your folder is ready, use the Nest CLI to scaffold your project. This will give you a clean structure and all the boilerplate setup you need to build with NestJS. Use the command below to do this and replace project name with what you would like to call your project.
If you're asked to choose a package manager, select either npm
or yarn
, whichever you prefer. Once that’s done, you’ll have a complete NestJS project initialized in the folder.
Installing dependencies
Once your project is generated, navigate into the newly created project folder. To do this, run the command below:
In that folder, you need to install some dependencies. These dependencies will be needed to send emails, load environment variables, and support event-based logic. For this, you need to install three packages:
@sendgrid/mail
: Allows your service to send transactional emails using Twilio SendGrid.@nestjs/config
: Lets you securely manage sensitive config values like API keys using a .env file.@nestjs/event-emitter
: Helps you trigger and listen for custom app events in a clean, decoupled way.
Creating the .env file
Before generating your SendGrid API key, you need to first create a .env file where you’ll securely store it. Now, in the root of your project( /project-name), create a new file called .env and add the following:
- SENDGRID_API_KEY: This key would be gotten on your SendGrid dashboard.
- SENDGRID_SENDER_EMAIL: This must be a verified sender identity in your account. You can add one via Marketing - Senders in the SendGrid dashboard
Getting your SendGrid credentials
Now that you have created your .env file, go to your SendGrid dashboard to get the required credentials.
- In the left-hand menu, go to Settings > API Keys
- Click Create API Key
- Name it something like Email Microservice Key or whatever you prefer
- Choose Full Access (for simplicity)
- Click Create & View, then copy the API key
- Paste it into your .env under
SENDGRID_API_KEY
You’ll also need to get your verified sender email. To get one:
- In the left-hand menu, go to Marketing > Senders
- Create a new sender if you don’t have one and wait for it to get approved
- If you already have an email sender set up, paste that into
SENDGRID_SENDER_EMAIL
Registering environment variables globally
Your app needs to access these variables you just added, to do that, update your main application module so NestJS can read the values in your .env file. Open src/app.module.ts and add the ConfigModule:
This way you can access any of the variables in your env anywhere in your code, securely.
Building the Email Module
The email module handles your logic for sending transactional emails using Twilio SendGrid. To do that, create a new folder called email, in the root of your project folder ( /project-name) and within it a file called email.service.ts.
Now within this file, you need to make some necessary imports:
@nestjs/common
: Provides the Injectable decorator used for services.@sendgrid/mail
: This is the official Node.js SDK for interacting with Twilio SendGrid.ConfigService
: To read the values in your .env file.
Next, you need to define your service class.
This sets up EmailService
as a NestJS provider and within the constructor:
- The
ConfigService
is injected. - The
SENDGRID_API_KEY
is being called from the .env file. - The SendGrid SDK is initialized with that key so it can send emails on behalf of your service.
Next, you need to set up the methods that will be needed for sending the email. It will consist of to
, which is the recipients email address, subject
the subject line of your email, templateID
the dynamic template ID of any template created on the Sendgrid dashboard and dynamicData
a key-value object for dynamic fields in your template e.g { name: ‘Williams’ }
Also included is a try and catch block. The try
statement attempts to send an email using SendGrid with the provided message object, while the catch
block logs any errors (including detailed SendGrid responses if available) and re-throws the error for further handling.
Next, you have to register the email module. To do this, create a new file in your email folder called email.module.ts
and do the following:
- Import your
ConfigModule
to allow access to your .env variables - Import you
EmailService
as a provider - Export your
EmailService
so that other parts of the app like your event handlers will be able to access it.
Creating the Event System
This section is responsible for the logic that listens for internal application events (like user.registered
, order.shipped
) and maps them to email templates. This makes the email system flexible and easy to extend, without modifying the core codebase. For example, if you're building an e-commerce app and add a gift card feature, the recipient can be notified when a gift card is sent to them. As long as the gift card event is mapped to an email template, the email notification will be sent automatically when the event is triggered.
Create the Event Mapper
Create a new directory called event, in the root of your project ( /project-name) and in this directory, a new file called event-mapper.ts. This is where all your registered service would sit. For the purpose of this article, we would assume you have 2 services called:
user.registered
payment.success
Here is what is going on above:
- You have defined a
type
calledEventConfig
which stores the following templateId
: the ID of a dynamic SendGrid email templatesubject
: the email subject line- toField: which field in the incoming data to use as the recipient’s email address
- Also events are being mapped and a
.get()
method is being provided to return the appropriate config for a given event type. This way new events can be onboarded by simply updating the map, no need to write more code or touch business logic.
Create the Event Service
Next, you need to create your event.service.ts in your events folder. So here, you are going to make use of the package EventEmitter2
, which is used to emit and listen for custom app events. You will also need to import the EventMapper
to match incoming events to templates
Next, you need to add the logic for handling these events and for dispatching them as well. So in the method, here is what is going to be happening:
- It will receive an incoming type and data payload
- Then use the event mapper to check if it’s supported
- The finally emit the event internally using NestJS’s event system (the reason why the event emitter package was installed)
With this, you are able to centralize how events are validated and dispatched, so that the email logic never touches the business logic directly.
Create the Event Module
The event module connects the event mapping and dispatch logic into a reusable feature module within NestJS.So, to do this, create a new file called event.module.ts in your event folder and paste the code below:
Here is what is going on:
EventEmitterModule.forRoot()
Initializes Nest’s built-in event system. This would allow your app to emit and listen to custom events within the application.EmailModule
imports the previously created email module, so that event handlers can trigger email sending.providers
registers the EventService and EventMapper as injectable services.exports
makes the EventService available to other modules that import the event module (such as the controller).
Listen for events in the Email Service
To respond when an event is emitted (e.g., user.registered
), you need to update the EmailService to listen for it and send the appropriate email. So head back to your email service file and update it with the code below:
Also don’t forget to import the decorator OnEvent
, which will allow the services to subscribe to specific events.
Here is what this does:
@OnEvent('user.registered')
registers this method to run whenever the user.registered event is emitted.payload: any
is the data associated with the event, and mostly includes fields like email and name.sendEmail(...)
uses the service’s existing method to send the email with the subject, template ID, and dynamic values.
Create a Controller to Trigger Events
Now you need a controller that exposes an HTTP endpoint so that other services or parts of your app can trigger internal events like user.registered. To do this in your app.controller.ts
file, add the following code below:
So this is what is going on:
@Controller()
registers this class as a NestJS controller.@Post('events')
creates a route at POST /events. A way to access the endpoint.@Body()
accepts a JSON payload with two fields:type
the event name (e.g. "user.registered" or “payment.success”)data
the data needed by the email template (e.g. { email, name })
Inside the triggerEvent()
method, the controller calls the handleIncomingEvent(
) method from EventService
. So what’s happening is that it emits the event into your internal system, which is then picked up by the listener in EmailService.
Finally, double-check your app.module.ts to make sure all the necessary services and controllers have been imported successfully.
Now time to test all you’ve put together so far. To do this you will test the register event you simulated earlier. The request body below, mimics what a backend service or another microservice might send when a new user registers.
In postman or any tool you are making use of, make a post request to
If your setup and SendGrid template are correct, you’ll get an email in seconds.


Now that all is good, you can move on to the webhook listener to track things such as opens, delivery etc.
Handle SendGrid Webhooks
SendGrid can notify your application about the status of sent emails via HTTP webhooks. These notifications include things like whether an email was delivered, opened, or bounced. In this section, you will learn how to expose a webhook endpoint that listens for those events.
Create the Webhook Controller
Start by creating a new folder called webhook in the root of your project (/project-name) and in that a file called webhook.controller.ts. In that file paste the code bellow:
So here is what this controller does:
@Post('sendgrid')
creates an endpoint at POST/webhook/sendgrid
.@Body() events: any[]
allows SendGrid to send webhook payloads as an array of event objects.@Headers('user-agent')
extracts and logs the User-Agent header to verify requests.- The loop logs each event type (like delivered, open, bounce) and the email it applied to.
- The response
{ received: true }
confirms to SendGrid that the webhook was processed.
Register the Controller
Next, you need to register the controller in your app.module.ts.
Test Webhooks Locally with Ngrok
Since SendGrid cannot reach your localhost, use Ngrok to expose your local server to the internet. Run your existing application in your terminal, and open another terminal in your vscode to run the command below to allow ngrok generate a url for you.
This creates a public HTTPS tunnel to your local port 3000, which your NestJS app uses.
This will generate a URL like:
You will need this to configure your Sendgrid Webhook for testing
If you do not have Ngrok installed, follow these steps:
Configure Webhook in SendGrid
To do this:
- Go to your SendGrid Dashboard.
- Go to Settings, Mail Settings, click on Event Webhook.
- Click on Create New Webhook
- Add a name and paste the link gotten from Ngrok, with this attached to the suffix - webhook/sendgrid
Your webhook should look like -
- Under Event Notifications, enable the actions to be posted. E.g, Processed, Delivered, Opened, Clicked or Bounced.
- Save the Configuration


Testing and troubleshooting
After completing the setup, it’s important to verify that the email microservice behaves as expected and that webhook events are received correctly.
Sending a test event
To trigger an email, send a POST request to the /events endpoint. This would simulate a real app firing an event like user.registered.
In postman or any tool you are making use of, make a post request to
Then in the body add this.
If the setup is correct:
- An email should be sent to adalove@gmail.com (or the email address you inputted)
- The email message should use your SendGrid dynamic template
- The terminal should show a user.registered event being received and an email sent
Viewing webhook logs
If webhooks are configured correctly and your server is exposed using Ngrok, SendGrid will send event notifications to /webhook/sendgrid. In your terminal, look for logs like:


These logs come from the WebhookController
and confirm that webhook events are getting to your application.
While testing here are some common issues you may encounter and how to resolve them:
Emails not arriving
- Check if
SENDGRID_SENDER_EMAIL
is verified in your account. - Make sure you're using a real, deliverable recipient email address.
- Template ID errors:
- Double-check that your template ID is copied correctly from the SendGrid dashboard.
- Make sure the template has at least one active version.
Webhook events not received
- Confirm your Ngrok URL is active and correct in the SendGrid webhook settings. To test, you can swap your
localhost:3000
url on Postman while testing to see if it delivers the email as it is supposed to. - Make sure ngrok is not closed or paused.
- Check that your WebhookController is registered in app.module.ts.
Unhandled event type error:
- If you see
Unhandled event type
, it means the EventMapper does not have a matching config for the type field sent in your request. - Add the event type and template config to event-mapper.ts.
Conclusion
Congratulations on setting up a complete event-driven email system using NestJS and Twilio SendGrid! You’ve learned how to trigger transactional emails from internal events, build a plug-and-play event mapper, and configure SendGrid webhooks to track delivery, opens, and bounces in real time.
If you’d like to explore the finished version of this microservice, you can check it out on GitHub here.
Email infrastructure should be treated with care. Remember email sending is really monitored, so try not to spam people or send the wrong thing. As a follow up you should consider checking out the Twilio Sendgrid's documentation and blog to learn more. Learn about features like IP warming and sender reputation management to improve the chances of your mails landing in your user' inbox and not getting blocked. You can also explore more advanced email flows like failure retries, A/B testing, and user-level analytics.
Thanks for reading!
Oghenevwede is a Senior Product Engineer with 8 years of obsessing about how things work on her phone and on the web. Her new obsession is health and wellness.
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.