RecipesEmail

Sending Email with SMTP

Wire Nodemailer as a factory service for transactional email — SMTP transport, HTML templates, and provider alternatives.

Send transactional email (welcome, reset, confirm) via SMTP using Nodemailer. One factory function, copy, adapt, done.

Before you start

  • Node.js 18+, Bun 1.0+, or Deno 1.30+
  • SMTP credentials (host, port, user, pass) from your provider
  • For Google: App Password (requires 2FA enabled)

Why SMTP?

SMTP is the universal protocol every email provider supports. One Nodemailer config works across Gmail, Resend, SendGrid, Mailgun, Postmark, and Amazon SES — only the server details change. No vendor lock-in, no SDK to swap when you migrate.

Setup

npm install nodemailer

Bun / Deno

nodemailer works on all three runtimes. On Deno, import from npm: import nodemailer from 'npm:nodemailer'.

Usage

1. Mail factory

Create a plain factory function — no class, no decorator.

src/mail.ts
import nodemailer from 'nodemailer';
import type { Transporter } from 'nodemailer';

export interface MailOptions {
  to: string | string[];
  subject: string;
  text?: string;
  html?: string;
}

export interface MailService {
  send(opts: MailOptions): Promise<string | null>;
  close(): void;
}

export function createMailService(): MailService {
  const transporter: Transporter = nodemailer.createTransport({
    host: process.env.SMTP_HOST ?? 'smtp.ethereal.email',
    port: Number(process.env.SMTP_PORT ?? 587),
    secure: process.env.SMTP_SECURE === 'true',
    auth: {
      user: process.env.SMTP_USER ?? '',
      pass: process.env.SMTP_PASS ?? '',
    },
  });

  return {
    async send({ to, subject, text, html }) {
      const info = await transporter.sendMail({
        from: process.env.MAIL_FROM ?? '"NextRush" <noreply@example.com>',
        to: Array.isArray(to) ? to.join(', ') : to,
        subject,
        text,
        html,
      });
      return info.messageId ?? null;
    },

    close() {
      transporter.close();
    },
  };
}

2. Use it

import { createMailService } from './mail';

const mail = createMailService();

await mail.send({
  to: 'user@example.com',
  subject: 'Welcome!',
  html: '<h1>Welcome to NextRush</h1>',
});

3. With DI container (optional)

If you use NextRush's container, register the factory as a provider.

src/providers.ts
import { createContainer } from '@nextrush/di';
import { createMailService } from './mail';

const container = createContainer();

container.register('MAIL', {
  useFactory: () => createMailService(),
});

// In another factory with dependencies:
container.register('USER_MAIL', {
  useFactory: (mail: MailService) => ({
    sendWelcome(email: string, name: string) {
      return mail.send({ to: email, subject: 'Welcome!', html: welcomeTemplate(name) });
    },
  }),
  inject: ['MAIL'],
});

export { container };

4. Class-based alternative (optional)

If you prefer @Service() decorators, the same logic works as a class.

src/mail.service.ts
import { Service } from 'nextrush/class';
import nodemailer from 'nodemailer';
import type { Transporter } from 'nodemailer';

interface SendOptions {
  to: string | string[];
  subject: string;
  text?: string;
  html?: string;
}

@Service()
export class MailService {
  private transporter: Transporter;

  constructor() {
    this.transporter = nodemailer.createTransport({
      host: process.env.SMTP_HOST ?? 'smtp.ethereal.email',
      port: Number(process.env.SMTP_PORT ?? 587),
      secure: process.env.SMTP_SECURE === 'true',
      auth: {
        user: process.env.SMTP_USER ?? '',
        pass: process.env.SMTP_PASS ?? '',
      },
    });
  }

  async send({ to, subject, text, html }: SendOptions) {
    const info = await this.transporter.sendMail({
      from: process.env.MAIL_FROM ?? '"NextRush" <noreply@example.com>',
      to: Array.isArray(to) ? to.join(', ') : to,
      subject,
      text,
      html,
    });
    return info.messageId ?? null;
  }

  close() {
    this.transporter.close();
  }
}
src/user-mail.service.ts
import { Service } from 'nextrush/class';
import { MailService } from './mail.service';

@Service()
export class UserMailService {
  constructor(private mail: MailService) {}

  async sendWelcome(email: string, name: string) {
    await this.mail.send({
      to: email,
      subject: 'Welcome to NextRush!',
      html: `<h1>Welcome, ${name}!</h1>`,
    });
  }
}

@Service() defaults to singleton scope — one transport for the lifetime of the process. Constructor injection wires everything automatically.

HTML Templates

Inline HTML strings get unmanageable at scale. Use a template function.

export function welcomeTemplate(name: string): string {
  return `
    <!DOCTYPE html>
    <html>
    <head><meta charset="utf-8"></head>
    <body style="font-family: sans-serif; padding: 24px;">
      <h1>Welcome, ${name}!</h1>
      <p>Thanks for joining NextRush.</p>
      <hr>
      <p style="color: #666;">The NextRush Team</p>
    </body>
    </html>
  `;
}

For complex layouts: Handlebars, MJML, or React Email.

Google SMTP (Gmail)

Regular password won't work

Google deactivated "less secure app access." You must use an App Password.

Step-by-step:

  1. Enable 2-Step Verification on your Google Account.
  2. Generate an App Password — select Mail + your device.
  3. Use the 16-character password as SMTP_PASS.
SMTP_HOST=smtp.gmail.com
SMTP_PORT=465
SMTP_SECURE=true
SMTP_USER=your-email@gmail.com
SMTP_PASS=abcd efgh ijkl mnop

Compatibility

RuntimeSupportedNotes
Node✅ 18+Native nodemailer
Bun✅ 1.0+Same as Node
Denoimport nodemailer from 'npm:nodemailer'

Alternatives

Swap SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS — code stays identical.

ServiceSMTPFree tierNotes
Resendsmtp.resend.com:587100/dayReact Email, modern SDK
SendGridsmtp.sendgrid.net:587100/dayTemplate editor
Mailgunsmtp.mailgun.org:587100/dayEU/US regions
Postmarksmtp.postmarkapp.com:587High deliverability
Amazon SESemail-smtp.region.amazonaws.com:58762k/mo from EC2Needs domain verify
Mailtrap (testing)sandbox.smtp.mailtrap.io:2525500/moCatch-all for dev

Troubleshooting

ErrorReasonFix
Invalid loginWrong password or App Password requiredGenerate App Password
connect ECONNREFUSEDWrong host or portCheck provider's SMTP docs
535 Authentication failedSMTP user/pass mismatchDouble-check SMTP_USER and SMTP_PASS
Sender address rejectedMAIL_FROM not authorizedUse the same address as SMTP_USER or verify domain
Message not sent, no errorPort blocked by hosting providerTry 587 instead of 465, or use HTTP API

Was this helpful?

On this page