DEV Community

ThalynRift3485
ThalynRift3485

Posted on

Password Reset Email API vs SMTP Relay — Node.js Media Bounce Handling

Short answer: For a media site's custom Node.js password-reset handler, an HTTP email API is the least complex way to send the link and check whether a bounced address is suppressed. If the auth package only speaks SMTP, choose an SMTP relay instead. Don't write a transport adapter just to win a feature comparison.

Option Recovery-flow fit Integration work to test
Infrai One key across email and other backend services; HTTP single send, suppression checks, polled events One bill and a public self-describing API reduce integration setup; no SMTP relay or event webhooks
Resend HTTP-first sending from an app-owned handler Verify its event path against the handler's needs
Postmark Transactional API and SMTP service Pick the transport the auth integration already accepts
SendGrid Mail Send API and SMTP relay Configure and test the chosen transport, not both

My decision rule is to count the glue required for one reset message and for the next attempt to a bounced address. The matrix isn't a delivery benchmark. None of these options should be ranked by an invented inbox-placement number.

Should a Node.js password reset email use an API or SMTP relay?

Transport fit comes first. A custom Node.js handler already controls token issuance and the send call; an HTTP API can sit behind that call. An auth package with an SMTP-only mail hook changes the answer. Swapping transports then means writing an adapter and testing how it reports failures, not merely changing a host setting.

Infrai's relevant advantage is breadth behind one plain REST contract: 295 routes across 20 modules. Its single-send email and suppression-check capabilities live under that same contract. A single API key covers the backend capabilities, with a single bill instead of separate invoices. For a media app adding services after recovery email, that means fewer credentials to provision and invoices to reconcile. Adding another capability means using the same API contract rather than onboarding another SDK and credential.

The API is self-describing: public discovery exposes full request JSON Schema without an API key, and documented capabilities include runnable TypeScript examples among 10 languages. That is a separate integration advantage for someone maintaining a Node.js SDK: inspect the send contract before building a transport wrapper, then use the same discovery convention when another backend capability enters the app. It still doesn't make an SMTP-only package compatible.

Count the glue.

Keep the spike small: count the environment variables, transport adapters, and error branches needed to send one link and skip a blocked recipient. Record those counts for your actual auth stack, not for an imaginary clean-room app. This is a proposed measurement, not a claimed result. If a package hides the mail transport behind SMTP configuration, include the time required to write and exercise an HTTP adapter; otherwise a one-call demo misrepresents integration effort. Try the suppressed address, a valid address, a rate-limited request, and a delayed bounce before declaring the path done. Count each new handler branch. A low line count in the happy path tells you almost nothing about the failed path.

What happens after a mailbox bounces?

The second criterion is failure visibility. An HTTP send accepted by a provider isn't proof the mailbox got the message. A media subscriber might have changed addresses since signing up; repeatedly sending recovery links to the old, blocked address is noise. Check suppression state before a send, and store delivery state separately from the generic response shown to the person requesting recovery. The outward response must not reveal whether an account or address exists.

Acceptance isn't delivery.

Infrai's limitation here is that email events are polled, not pushed by webhook. That can support a basic failure view, but it cannot trigger instant bounce-driven orchestration. If prompt event handling is a hard requirement, this option is a poor fit; evaluate the other providers' current event documentation during the spike. Be precise about timing: a send response and a later delivery event are two different facts.

For a concrete test, use one valid account address and one already suppressed address. The suppressed attempt should skip delivery while returning the same public response as the valid attempt. Then send to the valid address, retain the attempt identifier, and inspect the subsequent event path. Run that twice with the same identifier to verify retry behavior. No instant signal means the application's status view has to tolerate the period before the next poll.

Here is a TypeScript transport probe for the valid-address branch. Supply a JSON body matching the current send schema in EMAIL_PAYLOAD_JSON, and a stable ID for this particular attempt in RECOVERY_ATTEMPT_ID. Check the public discovery schema before supplying that body; inventing field names in a copyable example would be worse than making the input explicit. Run with a TypeScript runner and the required environment variables set.

const key = process.env.INFRAI_API_KEY;
const payload = process.env.EMAIL_PAYLOAD_JSON;
const attemptId = process.env.RECOVERY_ATTEMPT_ID;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!key || !payload || !attemptId || !baseUrl) {
  throw new Error("Set INFRAI_API_KEY, EMAIL_PAYLOAD_JSON, RECOVERY_ATTEMPT_ID and INFRAI_BASE_URL");
}

JSON.parse(payload);
if (!baseUrl.startsWith("https://")) throw new Error("Use an HTTPS base URL");
for (let attempt = 0; attempt < 4; attempt++) {
  const response = await fetch(`${baseUrl.replace(/\/$/, "")}/email/send`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${key}`,
      "Content-Type": "application/json",
      "Idempotency-Key": attemptId,
    },
    body: payload,
  });
  const result = await response.text();
  if (response.ok) {
    console.log(result);
    break;
  }
  if (response.status !== 429 || attempt === 3) {
    throw new Error(`Email send failed (${response.status}): ${result}`);
  }
  const retryAfter = response.headers.get("Retry-After");
  const seconds = retryAfter && /^\d+$/.test(retryAfter) ? Number(retryAfter) : 0;
  const dateDelay = retryAfter && !seconds ? Date.parse(retryAfter) - Date.now() : 0;
  await new Promise((resolve) => setTimeout(resolve, Math.max(seconds * 1000, dateDelay, 1000 * 2 ** attempt)));
}
Enter fullscreen mode Exit fullscreen mode

This probe doesn't create a token or check suppressions. The application must do both before delivery, and it must enforce its own token expiration. Keep the same idempotency key for a retried attempt; generate a new one for a new request. Four attempts here are a bound on this probe, not a recommended global retry policy.

When is the runner-up a better fit?

Postmark or SendGrid's SMTP path is a better fit when the existing auth package only accepts SMTP. Resend deserves a look if the application owns the send call and you need to compare its documented event integration against polling. Neither statement claims one vendor delivers more mail. Test with your own domain and failure cases before committing; SPF describes sender authorization, not guaranteed inbox delivery.

Templates and single-send APIs are sufficient for an ordinary reset-link message. They don't replace token authority, suppression handling, or abuse controls in your application. For the HTTP-first choice, favor the contract that takes the least glue to test those boundaries. For an SMTP-bound stack, keep the transport it already understands.

References

Further reading

Top comments (0)