NestJS + MailKite
NestJS doesn't ship its own mailer — the ecosystem convention is
@nestjs-modules/mailer, which wraps nodemailer. MailKite plugs
straight into it via nodemailer-mailkite-transport: one
transport option, no MailKite-specific NestJS package needed.
What you need
- A verified domain with SPF + DKIM published
- Your API key (
mk_live_…) - NestJS 10/11 with
@nestjs/platform-express(or fastify)
Install
npm install @nestjs-modules/mailer nodemailer nodemailer-mailkite-transport nodemailer-mailkite-transport pulls in the mailkite
SDK automatically. Pick a template engine
(Handlebars, Pug, EJS) only if you want templated emails — plain HTML/text
strings work with no extra dependency.
Configure .env
# .env
MAILKITE_API_KEY=mk_live_...
MAIL_FROM="MyApp <hello@yourdomain.com>" Register the mailer module
MailerModule.forRoot()'s transport option accepts
any nodemailer Transport object — and that's exactly what
mailkiteTransport(...) returns. No adapter code, no custom
provider:
// src/mail/mail.module.ts
import { Module } from '@nestjs/common';
import { MailerModule } from '@nestjs-modules/mailer';
import { mailkiteTransport } from 'nodemailer-mailkite-transport';
import { MailController } from './mail.controller';
@Module({
imports: [
MailerModule.forRoot({
transport: mailkiteTransport({
apiKey: process.env.MAILKITE_API_KEY,
}),
defaults: {
from: process.env.MAIL_FROM,
},
}),
],
controllers: [MailController],
})
export class MailModule {} Send email
Inject the standard MailerService anywhere in your app and
call sendMail() — it resolves with MailKite's own message id:
// src/mail/mail.controller.ts
import { Body, Controller, Post } from '@nestjs/common';
import { MailerService } from '@nestjs-modules/mailer';
@Controller('mail')
export class MailController {
constructor(private readonly mailerService: MailerService) {}
@Post('send')
async send(@Body() dto: { to: string; subject: string; html: string }) {
const info = await this.mailerService.sendMail({
to: dto.to,
subject: dto.subject,
html: dto.html,
});
// info.messageId is MailKite's own message id (msg_…)
return { messageId: info.messageId, status: info.status };
}
} Test it
curl -X POST http://localhost:3000/mail/send \
-H "Content-Type: application/json" \
-d '{"to":"ada@example.com","subject":"Hello from Nest","html":"<p>Hi!</p>"}'
A successful response looks like
{"messageId":"msg_…","status":"queued"}.
Full runnable starter
Want to see it wired end to end — including a static test form so you can
send without curl? Clone the
NestJS starter:
a POST /mail/send controller, request validation, and a
zero-dependency HTML form served at /.
Why no dedicated NestJS package
@nestjs-modules/mailer already provides the module system,
dependency injection, and async configuration NestJS apps expect from a
mailer. Wrapping that in a MailKite-specific MailKiteModule
would save one import and one config key at the cost of a whole package to
maintain — not worth it. The transport-level integration is the right
amount of code here.
Troubleshooting
ERR_PACKAGE_PATH_NOT_EXPORTEDon install — make sure you're onnodemailer-mailkite-transport@0.1.1or later; earlier versions had a CommonJS packaging bug that brokerequire()-based NestJS builds.- 401 invalid token —
MAILKITE_API_KEYisn't a realmk_live_…key, or it's revoked. Check Settings → API key in the dashboard. - Send fails with a domain/from error —
MAIL_FROMmust be an address on a verified domain with SPF + DKIM published. - Attachments with
cid(inline images) or customheadersthrow — the MailKite send API has no equivalent for those nodemailer fields; the transport refuses instead of silently dropping them. Host inline images by URL instead.
See the Send API docs for the full payload reference, or Inbound webhooks to receive email in a NestJS controller.