Interop with @nestjs/mail
The NestJS team publishes an official mail package, @nestjs/mail by Kamil Myśliwiec: a dependency-free SMTP client and MIME builder with HTTP transports, typed mail classes, retries and an in-memory test transport. @nestjs-modules/mailer v4 does not compete with it on its own ground; it plugs into it. The @nestjs-modules/mailer/nestjs-mail entry point gives @nestjs/mail access to the whole nodemailer ecosystem, to this module's delivery features (failover, interception, suppression, plugins, events) and to its template engines, and mail classes are shaped so the same class works with both libraries.
When to use which
- Starting fresh, SMTP or one of its HTTP providers, logic-less templates are enough:
@nestjs/mailalone is a good fit, with no dependencies. - You need a transport it does not have (SES SDK, sendmail, Mailgun, Brevo, any nodemailer transport or plugin): use
@nestjs/mailwithNodemailerMailTransport. - You want Mailgun with
@nestjs/mail: wrap this module'sMailgunTransportinNodemailerMailTransport.@nestjs/mailhands over raw MIME, which onlyMailgunTransportandSesTransportaccept among this module's HTTP transports; the JSON ones (Resend, Postmark, SendGrid, Brevo) refuse raw messages, so with@nestjs/mailuse its own Resend, Postmark and SendGrid transports. (MailerServiceTransportforwards the same raw message, so the JSON transports can't be used behind it either.) - You want
@nestjs/mail's API but this module's delivery features (named transporters, failover, staging interception, suppression lists, events, health checks): useMailerServiceTransport. - You want Handlebars, Pug, EJS, MJML, Liquid or Nunjucks templates in
@nestjs/mail: useTemplateAdapterEngine. - You already use
@nestjs-modules/mailer: keep it. v4 has mail classes, retries and a test mailbox of its own. See the comparison for the details.
Installation
pnpm add @nestjs/mail @nestjs-modules/mailer nodemailer@nestjs/mail is an optional peer dependency: you only need it for this entry point. It is published as ES modules; a CommonJS Nest app loads it through Node.js' require(esm), which needs Node.js >= 20.19.
import {
MailerServiceTransport,
NodemailerMailTransport,
TemplateAdapterEngine,
} from '@nestjs-modules/mailer/nestjs-mail';This entry point doesn't import @nestjs/mail's types: it declares structural equivalents (MailMessageLike, MailTransportLike, MailTemplateEngineLike, MailTransportResultLike, …, exported from the subpath), so CommonJS projects compiled with moduleResolution: node16/nodenext build without resolving the ESM-only package.
1. Nodemailer transports for @nestjs/mail
NodemailerMailTransport is a @nestjs/mail transport that delivers through nodemailer. It takes anything MailerModule's transport takes (an SMTP URL or options, a nodemailer transport such as SES or sendmail, a community transport) or an existing nodemailer Transporter.
import { Module } from '@nestjs/common';
import { MailModule } from '@nestjs/mail';
import { NodemailerMailTransport } from '@nestjs-modules/mailer/nestjs-mail';
@Module({
imports: [
MailModule.forRoot({
transport: new NodemailerMailTransport({
pool: true,
host: 'smtp.example.com',
port: 587,
auth: { user: 'user', pass: 'pass' },
}),
from: 'App <app@example.com>',
}),
],
})
export class AppModule {}import { Injectable } from '@nestjs/common';
import { Mailer } from '@nestjs/mail';
@Injectable()
export class NotificationsService {
constructor(private readonly mailer: Mailer) {}
notify(email: string) {
return this.mailer.send({ to: email, subject: 'Hello', html: '<p>Hello</p>' });
}
}- The message is sent exactly as
@nestjs/mailbuilt it: its MIME source and SMTP envelope are handed to nodemailer asraw, so theMessage-ID, DKIM signature and Bcc handling stay@nestjs/mail's. - Errors are marked with
permanentusing the same rules as this module (SMTP 5xx is permanent,ECONNECTIONis transient…), so@nestjs/mail's retries skip what cannot succeed. verify()checks the connection and credentials (for a health check) andclose()closes pooled connections.acceptedcomes from the transport's result, or from the delivered envelope when the transport reports none (stream and JSON transports).- The underlying nodemailer transporter is available as
transport.transporter.
2. Deliver through MailerService
MailerServiceTransport sends @nestjs/mail messages through this module's MailerService. You write emails with @nestjs/mail's Mailer and mail classes; delivery gets MailerModule's named transporters, failover, interception, suppression, nodemailer plugins and events. Import both modules and pass the class (Nest instantiates it and injects MailerService):
import { Module } from '@nestjs/common';
import { MailModule } from '@nestjs/mail';
import { MailerModule } from '@nestjs-modules/mailer';
import { MailerServiceTransport } from '@nestjs-modules/mailer/nestjs-mail';
@Module({
imports: [
MailerModule.forRoot({
transports: {
ses: { host: 'email-smtp.eu-west-1.amazonaws.com', port: 587, auth: { user: 'key', pass: 'secret' } },
backup: { host: 'smtp.backup-provider.com', port: 587, auth: { user: 'user', pass: 'pass' } },
},
failover: ['backup'],
intercept: {
enabled: process.env.APP_ENV === 'staging',
to: 'qa@example.com',
allow: ['@example.com'],
},
}),
MailModule.forRoot({
// A transporter from `transports`; pass `MailerServiceTransport` for the default `transport`
transport: MailerServiceTransport.for('ses'),
from: 'App <app@example.com>',
}),
],
})
export class AppModule {}MailerServiceTransportusesMailerModule's defaulttransport;MailerServiceTransport.for('name')returns a transport class bound to a named transporter.- Retries stay with
@nestjs/mail: this transport sends withretry: false, so attempts are never multiplied. Failover between transporters still happens inside one attempt. @nestjs/mail'sidempotencyKeyandsignalare passed on.MailerTestModuleworks here too: itsTestMailboxcaptures the messages, and matchestoandsubjectread from the MIME source.
Known limit
@nestjs/mail hands over a finished MIME message. Interception and suppression rewrite its envelope, which decides where it is delivered, but not the To: header inside the message, and subjectPrefix is not applied. An intercepted message reaches the QA inbox showing the original recipient in To:.
3. Template engines for @nestjs/mail
TemplateAdapterEngine lets @nestjs/mail render templates with this module's adapters: Handlebars (helpers, partials, layouts), Pug, EJS, MJML, Liquid and Nunjucks, with CSS inlining.
import { Module } from '@nestjs/common';
import { MailModule } from '@nestjs/mail';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/adapters/handlebars.adapter';
import { NodemailerMailTransport, TemplateAdapterEngine } from '@nestjs-modules/mailer/nestjs-mail';
import { join } from 'node:path';
@Module({
imports: [
MailModule.forRoot({
transport: new NodemailerMailTransport('smtps://user:pass@smtp.example.com'),
from: 'App <app@example.com>',
templates: new TemplateAdapterEngine({
adapter: new HandlebarsAdapter({ year: () => new Date().getFullYear() }),
dir: join(__dirname, 'templates'),
options: {
partials: { dir: join(__dirname, 'templates/partials') },
},
}),
}),
],
})
export class AppModule {}await this.mailer.send({
to: 'ada@example.com',
subject: 'Welcome',
template: 'welcome',
context: { name: 'Ada' },
locale: 'es-CO',
});| Option | Description |
|---|---|
adapter | One of this module's adapters |
dir | The templates directory |
dirs | More directories to search |
templateOptions | Compile options for the engine (what MailerOptions.template.options holds) |
options | Runtime options such as Handlebars partials (what MailerOptions.options holds) |
Localized templates are found in both layouts: a folder per locale (es/welcome.hbs, this module's convention) and a suffix (welcome.es.hbs, @nestjs/mail's). A region falls back to its language and then to the unlocalized template: es-CO tries es-CO/welcome, welcome.es-CO, es/welcome, welcome.es, then welcome. A missing or broken template is marked permanent, so it is not retried.
The engine returns the rendered HTML; @nestjs/mail derives the text part from it.
Migrating mail classes
Both libraries define a mail class as an injectable with render(data, { locale, to }), with the same context shape (to is a list of { name?, address }), and accept the same core fields: subject, html (a string or SafeHtml) or text, template and context, attachments, headers, from, replyTo. A class written for one works with the other and type-checks with both:
import { Injectable } from '@nestjs/common';
import { Mailable, MailableContext } from '@nestjs-modules/mailer';
@Injectable()
export class OrderShippedMail implements Mailable<{ id: number }> {
render(order: { id: number }, { locale, to }: MailableContext) {
return {
subject: `Order #${order.id} shipped`,
template: 'order-shipped',
context: { order, locale, customer: to[0]?.name },
};
}
}
// @nestjs-modules/mailer
await mailerService.send(OrderShippedMail, { to: 'ada@example.com', data: { id: 42 } });
// @nestjs/mail
await mailer.send(OrderShippedMail, { to: 'ada@example.com', data: { id: 42 } });The differences to watch for:
- The
htmltag: both libraries have one, andMailerServiceaccepts@nestjs/mail'sSafeHtmlashtml. Don't mix them inside one template, though: this module'shtmltag only recognizes its ownSafeHtmlas already escaped, so a nested@nestjs/mailvalue would be escaped again. - Template syntax:
@nestjs/mail'sFileTemplateEngineuses its own logic-less HTML templates; this module uses the adapter you configure. UseTemplateAdapterEngineto share one set of templates between both. - Extra fields: a class for this module may also return
textTemplate,transporterName,icalEventandunsubscribe, which@nestjs/maildoes not know. - Instances:
MailerService.send()also accepts a mail class instance;@nestjs/mailtakes the class. - Resolution: both use a class registered as a provider. For a class that isn't one,
@nestjs/mailonly sees global providers, whileMailerServiceresolves its dependencies from anywhere in the app. See how the class is resolved.
To test a @nestjs/mail setup that delivers through MailerServiceTransport, import MailerTestModule.register() instead of MailerModule and assert with TestMailbox. See Testing.