How to Send Transactional Emails in Node.js (Without Nodemailer)

Send password resets, welcomes, and receipts from Node.js with fetch and Notify — no Nodemailer, no SMTP ports, works in Express and Next.js.

Short answer: In Node.js 18+, send transactional email with built-in fetch to a provider HTTPS API. Skip Nodemailer unless you are stuck on SMTP. With Notify, one helper covers Express, Fastify, Nest, and Next.js Route Handlers.

Key takeaways

  • Nodemailer is optional; SMTP from your app is usually the wrong default in 2026
  • Keep NOTIFY_API_KEY on the server only
  • One shared sendEmail helper beats copy-pasted fetch calls
  • Use sandbox/test while DNS is pending; verified domain for production from
  • Notify Free = 1,000 emails/mo; Pro = $10 / 10,000

Background: SMTP vs email APIs.

Why skip Nodemailer for new Node apps

Nodemailer is a capable SMTP client. New product code usually wants:

  • No relay host / port / secure flag matrix on every deploy target
  • HTTP status codes and JSON error bodies
  • Provider logs and webhooks
  • The same helper on Edge or Workers later

If Nodemailer + a provider SMTP relay already works in production, you can leave it. For greenfield transactional mail — password resets, magic links, receipts — call the API.

Prerequisites

NOTIFY_API_KEY=your_api_key_here
APP_URL=http://localhost:3000

Never put the key in frontend bundles or NEXT_PUBLIC_* vars.

Step 1 — Shared Notify helper

// lib/notify.ts
export type SendEmailInput = {
  to: string;
  subject: string;
  message: string;
  from?: string;
};

export async function sendEmail({
  to,
  subject,
  message,
  from = 'noreply@your-verified-domain.com'
}: SendEmailInput) {
  const response = await fetch('https://notify.cx/api/email/send', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': process.env.NOTIFY_API_KEY!
    },
    body: JSON.stringify({ from, to, subject, message })
  });

  if (!response.ok) {
    throw new Error(`Notify error: ${await response.text()}`);
  }

  return response.json();
}

message accepts plain text or HTML. You own the content layer — string templates, your own components, Markdown→HTML. Notify delivers. That split is deliberate: Why email APIs should stay small.

Also: How to use Notify with Node.js.

Step 2 — Express example

import express from 'express';
import { sendEmail } from './lib/notify.js';

const app = express();
app.use(express.json());

app.post('/api/email/welcome', async (req, res) => {
  const { to, name } = req.body ?? {};
  if (!to || typeof to !== 'string') {
    return res.status(400).json({ error: 'to is required' });
  }

  // Require a session / admin auth in production — never open-relay.

  try {
    const data = await sendEmail({
      to,
      subject: 'Welcome',
      message: `<h1>Welcome${name ? `, ${name}` : ''}</h1>
        <p>Thanks for joining. <a href="${process.env.APP_URL}">Open the app</a>.</p>`
    });
    res.json(data);
  } catch (err) {
    console.error(err);
    res.status(500).json({ error: 'Failed to send email' });
  }
});

app.listen(3000);

Full Express guide: Notify with Express.

Step 3 — Password reset (production pattern)

import { createHash, randomBytes } from 'crypto';
import { sendEmail } from './lib/notify.js';

export async function requestPasswordReset(email: string) {
  const user = await findUserByEmail(email);

  // Always return the same client-facing shape (no email enumeration).
  if (user) {
    const token = randomBytes(32).toString('hex');
    await saveResetToken({
      userId: user.id,
      tokenHash: createHash('sha256').update(token).digest('hex'),
      expiresAt: new Date(Date.now() + 60 * 60 * 1000)
    });

    const resetUrl = `${process.env.APP_URL}/reset-password?token=${token}`;

    await sendEmail({
      to: email,
      subject: 'Reset your password',
      message: `
        <h1>Reset your password</h1>
        <p><a href="${resetUrl}">Choose a new password</a></p>
        <p>This link expires in one hour. If you did not request this, ignore this email.</p>
      `
    });
  }

  return { ok: true, message: 'If that email exists, we sent a reset link.' };
}

Rate-limit this endpoint by IP and email. Full guide: How to send password reset emails · longer SaaS walkthrough: Build auth email flows.

Step 4 — Next.js App Router

Same helper from a Route Handler or Server Action — never from a Client Component.

// app/api/email/welcome/route.ts
import { sendEmail } from '@/lib/notify';
import { requireUser } from '@/lib/auth';

export async function POST() {
  const user = await requireUser();
  const data = await sendEmail({
    to: user.email,
    subject: 'Welcome',
    message: `<p>Hi ${user.name}, you’re in.</p>`
  });
  return Response.json(data);
}

Docs: Notify + Next.js · Server Action recipe.

Step 5 — Sandbox before DNS is ready

While domain verification is pending:

await fetch('https://notify.cx/api/email/send/test', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': process.env.NOTIFY_API_KEY!
  },
  body: JSON.stringify({
    from: 'noreply@your-verified-domain.com',
    to: 'you@example.com',
    subject: 'API shape check',
    message: '<p>Sandbox rehearsal</p>'
  })
});

Sandbox vs production.

Receipts and other transactional types

The helper does not change — only the HTML and trigger do:

Common mistakes

  1. Calling Notify from the browser
  2. Unauthenticated routes that accept arbitrary to / HTML
  3. Production from on an unverified domain
  4. Swallowing non-OK responses
  5. Building SES SMTP “for later” before shipping auth email

Why Notify fits Node teams

  • Tiny mental model — one fetch, no SDK required
  • Flat pricing you can explain to a founder (pricing)
  • Logs on all plans; webhooks on Pro/Scale
  • Same API from Express today and Workers tomorrow
  • Honest best for / not for: transactional yes; newsletters and template studios no

Coming from Nodemailer SMTP: Migrate from SMTP. Comparing providers: Resend vs Postmark vs SES vs Notify.

Next steps

  1. Quick start
  2. Verify a domain
  3. Ship password reset + welcome
  4. Read best practices 2026

Frequently asked questions

How do I send email from Node.js without Nodemailer?

Use Node 18+ fetch against a transactional email API. With Notify: POST https://notify.cx/api/email/send with an x-api-key header and JSON body (to, from, subject, message). Keep the key on the server.

Does this work with Express and Next.js?

Yes. The same send helper works in Express routes, Fastify, Nest, and Next.js Route Handlers or Server Actions. Never call it from a Client Component. Guides: Express, Next.js.

How do I test before verifying a domain?

Use Notify’s sandbox path POST /api/email/send/test with the same body shape, or the dashboard guided test. Details: Sandbox vs production.

How much does Notify cost for Node apps?

Free includes 1,000 emails/month. Pro is $10/month for 10,000. Scale is $50/month for 100,000. See pricing.