Sending Email From Node.js: Separate the Request From the Send

Sending Email From Node.js: Separate the Request From the Send

Build a Node.js email workflow with safe message previews, SMTP configuration, durable jobs and clear handling of retries and delivery outcomes.

MailBlastr Team

TL;DR

  • Keep email submission on the server and read credentials from your deployment's secret configuration.
  • Start by building and inspecting a message without sending it, then test against a mailbox you control.
  • Save a notification identity and submission result so retries can be handled deliberately.
  • Use delivery events for the final outcome; a successful library call does not prove inbox placement.

Separate message construction from delivery

A Node.js application often begins with a single send function. That is enough for a demonstration, but a production feature also needs a stable message identity, recipient validation, error handling and a way to inspect what was sent.

Create one function that builds the message from trusted application data and another that submits it through your chosen transport. This lets you test the subject, recipient and body without connecting to an external service. It also makes a later provider change less disruptive.

The examples here use Nodemailer and standard SMTP concepts. They are not a MailBlastr-specific endpoint or SDK contract. Check your provider's current connection details before adapting the transport.

Inspect a message locally first

Install Nodemailer in a Node.js project and save this original example as an ES module, such as preview-email.mjs. Its JSON transport builds a message without delivering it:

import nodemailer from "nodemailer";

const preview = nodemailer.createTransport({ jsonTransport: true });
const message = {
  from: "Example App <notifications@example.com>",
  to: "test-recipient@example.com",
  subject: "Your project invitation is ready",
  text: "Open your account to review the project invitation.",
  html: "<p>Open your account to review the project invitation.</p>",
};
const result = await preview.sendMail(message);
console.log(result.message);

The example addresses are placeholders. Keep preview output limited to test data; production message bodies can contain personal information. Inspect the generated recipient, subject, text and HTML before adding real delivery.

Configure a server-side SMTP transport

For a provider that documents port 587 with STARTTLS, a configuration can look like this:

const transport = nodemailer.createTransport({
  host: process.env.SMTP_HOST,
  port: 587,
  secure: false,
  requireTLS: true,
  auth: {
    user: process.env.SMTP_USER,
    pass: process.env.SMTP_PASSWORD,
  },
  connectionTimeout: 10000,
  socketTimeout: 30000,
});
await transport.verify();

Validate that required environment variables exist before constructing the transport. For a provider requiring implicit TLS on port 465, use its corresponding settings instead. Do not disable certificate verification to make an error disappear.

Nodemailer's SMTP documentation explains TLS configuration and notes that verify() checks connectivity and authentication, not whether a particular sender address will be accepted. A controlled send is still necessary.

Create a durable notification record

Before submission, save the logical event that requires the email: for example, an invitation identified by a database record. Store its recipient, template version and status. After submission, save the provider identifier and the outcome you actually observed.

If the network times out, mark the submission uncertain until you can reconcile it. Do not assume the provider received nothing. Our email idempotency guide describes the identity and retry decisions that prevent duplicate notifications.

For slower delivery work, use a durable background job rather than keeping a user request open indefinitely. Commit the business event before the worker sends the notification. A database outbox can help connect those two responsibilities without sending mail for a transaction that later rolls back.

Test failure paths before deployment

TestExpected application behaviour
Missing SMTP configurationFail clearly before attempting submission
Invalid controlled recipientRecord the final failure and its reason
Duplicate job executionPreserve one intended notification identity
Temporary provider problemApply bounded retry or reconciliation rules
Duplicate delivery eventUpdate state without repeating side effects

Test from the deployed environment as well as locally. Network restrictions, missing secrets and different file paths can produce failures that a laptop test will not reveal. Keep diagnostic logs useful but limited: notification identifier, provider identifier, outcome and a redacted error code.

Finish the user-facing flow

Tell users what the application knows. “Request received” is appropriate when a job is queued. “Message submitted” describes provider acceptance. Neither should automatically become “Email reached your inbox.”

Provide a clear next step when delivery fails, such as checking an address through the account settings. Compare the MailBlastr integration options with your application's requirements, and keep the transport behind a small interface so the rest of the product follows the notification lifecycle rather than a particular library call.