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 nodemailerBun / 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.
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.
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.
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();
}
}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:
- Enable 2-Step Verification on your Google Account.
- Generate an App Password — select Mail + your device.
- 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 mnopCompatibility
| Runtime | Supported | Notes |
|---|---|---|
| Node | ✅ 18+ | Native nodemailer |
| Bun | ✅ 1.0+ | Same as Node |
| Deno | ✅ | import nodemailer from 'npm:nodemailer' |
Alternatives
Swap SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS — code stays identical.
| Service | SMTP | Free tier | Notes |
|---|---|---|---|
| Resend | smtp.resend.com:587 | 100/day | React Email, modern SDK |
| SendGrid | smtp.sendgrid.net:587 | 100/day | Template editor |
| Mailgun | smtp.mailgun.org:587 | 100/day | EU/US regions |
| Postmark | smtp.postmarkapp.com:587 | — | High deliverability |
| Amazon SES | email-smtp.region.amazonaws.com:587 | 62k/mo from EC2 | Needs domain verify |
| Mailtrap (testing) | sandbox.smtp.mailtrap.io:2525 | 500/mo | Catch-all for dev |
Troubleshooting
| Error | Reason | Fix |
|---|---|---|
Invalid login | Wrong password or App Password required | Generate App Password |
connect ECONNREFUSED | Wrong host or port | Check provider's SMTP docs |
535 Authentication failed | SMTP user/pass mismatch | Double-check SMTP_USER and SMTP_PASS |
Sender address rejected | MAIL_FROM not authorized | Use the same address as SMTP_USER or verify domain |
| Message not sent, no error | Port blocked by hosting provider | Try 587 instead of 465, or use HTTP API |
- 🛠 Guide: Environment Configuration — managing SMTP credentials in
.env - 📚 Reference:
@nextrush/diContainer —createContainer,register,inject - 🧠 Concept: Dependency Injection — scopes and wiring patterns