Menu
Accedi Crea account
Guide

Transactional Email from Node.js: REST API vs SMTP, Pros and Cons

Most Node.js apps ship transactional email via Nodemailer over SMTP. Almost as many would be better served by a REST API. Here is how to choose and how to implement both right.

12 May 2025 · 10 min read · Target SMTP

Node.js applications send transactional email through one of two shapes: SMTP via nodemailer, or a provider REST API via HTTPS. Both work. They have very different operational characteristics. Pick the wrong one and you will end up with mysterious timeouts in production, retries that double-send, or rate limits you cannot debug.

This article compares the two approaches across the dimensions that actually matter at 3 AM: connection pooling, retry semantics, idempotency, error visibility, and graceful degradation. Then it shows the exact code for both.

The Two Shapes

SMTP via Nodemailer

Your app opens a TCP connection to smtp.provider.com:587, performs STARTTLS, authenticates, and exchanges MAIL FROM, RCPT TO, DATA commands. The protocol is text and stateful. The provider returns a 250 OK with a queue ID or a 4xx/5xx error.

REST API over HTTPS

Your app POSTs a JSON body to https://api.provider.com/v1/email. The provider returns 200 with a message ID or 4xx/5xx with an error object.

Connection Model

SMTP requires a persistent TCP connection. Open the connection, send N messages, close. For high throughput you want a pool of pre-warmed connections. nodemailer handles this for you with pool: true.

import nodemailer from "nodemailer";

const transporter = nodemailer.createTransport({
  host: "smtp.targetsmtp.it",
  port: 587,
  secure: false,           // STARTTLS
  auth: { user: USER, pass: PASS },
  pool: true,
  maxConnections: 5,
  maxMessages: 100,        // per connection
  rateLimit: 14,           // sends per second per connection
  socketTimeout: 30000,
});

REST has no pooling: each request is independent. Node's fetch reuses the underlying HTTP/1.1 or HTTP/2 connection automatically via the global agent. This is simpler and matches the way the rest of your app talks to APIs.

Latency and Throughput

For a single message, REST is faster: one round trip vs four for SMTP (EHLO, STARTTLS, AUTH, MAIL/RCPT/DATA). For a stream of messages, SMTP with a warm pool is faster per message because the handshake is amortised across many messages.

In practice, most Node.js apps are not throughput-bound at the email layer — they are bound by application logic. So latency and reliability matter more than peak throughput.

Error Visibility

SMTP errors come back as SMTP response codes wrapped in nodemailer errors:

try {
  await transporter.sendMail({...});
} catch (err) {
  // err.responseCode (e.g. 550), err.response (full SMTP text)
  if (err.responseCode === 550) markAsHardBounce(...);
}

REST errors come back as HTTP status + JSON body:

const res = await fetch("https://api.targetsmtp.it/v1/email", {...});
if (!res.ok) {
  const body = await res.json();
  // body.code === "invalid_recipient", body.message === "..."
}

The REST error model is easier to parse and to map to your application's error taxonomy. The SMTP model gives you the actual response from the receiving server (when the provider passes it through) which is invaluable for deliverability debugging.

Idempotency

This is the dimension most teams forget. Your code fails partway through a send. Did the provider accept it? If you retry, will the user get two welcome emails?

SMTP gives you no idempotency primitive. If the connection drops after DATA but before 250 OK, you do not know. Retrying may duplicate. Not retrying may lose.

REST APIs typically support an idempotency key:

await fetch("https://api.targetsmtp.it/v1/email", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${API_KEY}`,
    "Idempotency-Key": `welcome-${user.id}-v1`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({...})
});

The provider keeps the idempotency key for ~24 hours. Retries with the same key return the same response without resending. This is the single biggest reliability advantage of REST over SMTP.

Retry Semantics

For SMTP, you must:

  1. Catch the error.
  2. Classify it as transient (4xx) or permanent (5xx).
  3. Schedule a retry for transient.
  4. Accept that you may double-send if the connection died mid-flight.

For REST with idempotency keys, just retry on any 5xx or network error. The key handles deduplication.

async function sendWithRetry(payload, idempotencyKey, attempt = 1) {
  try {
    const res = await fetch("https://api.targetsmtp.it/v1/email", {
      method: "POST",
      headers: {
        "Authorization": `Bearer ${API_KEY}`,
        "Idempotency-Key": idempotencyKey,
        "Content-Type": "application/json"
      },
      body: JSON.stringify(payload),
      signal: AbortSignal.timeout(10000)
    });
    if (res.status >= 500 && attempt < 5) {
      await sleep(Math.min(60000, 1000 * 2 ** attempt));
      return sendWithRetry(payload, idempotencyKey, attempt + 1);
    }
    return res;
  } catch (err) {
    if (attempt < 5) {
      await sleep(Math.min(60000, 1000 * 2 ** attempt));
      return sendWithRetry(payload, idempotencyKey, attempt + 1);
    }
    throw err;
  }
}

Graceful Degradation

If your provider is degraded, you want to either queue locally or fail gracefully. SMTP makes queueing natural — you push messages into a local queue and a worker drains it via the pool. REST makes it less natural because you typically call the API inline from a request handler.

💡 Tip: With REST, push email "jobs" to your background queue (BullMQ, SQS, etc.) and call the API from the worker. Never call the API inline from an HTTP handler. The latency variance will hurt your user-facing P99.

Attachments and Large Bodies

SMTP has no built-in size limit but every server has one (typically 25-50 MB after base64 encoding). REST APIs usually publish a smaller limit (10 MB JSON body) with separate endpoints for attachments. If you frequently send invoices over 5 MB, validate your provider's API limit before committing.

Security

SMTP credentials are long-lived username/password pairs. Rotating them requires touching every deployment. REST API keys can be scoped (read-only, send-only, per-domain), rotated frequently, and revoked instantly. For multi-team environments REST wins on security.

Observability

REST gives you a JSON message ID at send time. SMTP gives you a free-form text response that may or may not contain a queue ID. With REST you can pass a custom X-Tag header that the provider echoes in webhooks and logs. With SMTP you must add your own Message-ID header and rely on log correlation.

The Decision

You should pick SMTP ifYou should pick REST if
You already have nodemailer wired upYou are starting fresh
You ship from many low-throughput sourcesYou ship from one high-throughput service
Your provider lacks an APIYou want idempotency keys
You need to use legacy SMTP pluginsYou want scoped API keys

Hybrid

Many production stacks run SMTP for legacy senders (the WordPress plugin, the contact form) and REST for the main application. There is nothing wrong with this. Just make sure both authenticate as the same domain so reputation accumulates in one bucket.

Closing

For greenfield Node.js services, REST with idempotency keys is the better default. SMTP is fine but loses on the operational dimensions that bite you at 3 AM. Target SMTP provides both transports with the same authentication and the same observability. The Send-Time Firewall applies to both: the same policy (recipient allowlist, hold-window, idempotency check, content guardrail) runs whether the request arrived via REST or SMTP.

Tag #nodejs #smtp #api #integration

Related posts