DEV Community

leiferiksson8493
leiferiksson8493

Posted on

Password Reset Email APIs vs SMTP Relays: Transactional Provider Trade-offs Explained

For a beginner Node.js app, use a password-reset email API when your auth flow can make an HTTPS request; choose an SMTP relay when your framework or auth package only knows SMTP. That integration constraint matters more than a feature checklist, because a reset link is a short, transactional message and the wrong transport creates a rewrite before you have users.

I run a one-person SaaS, so I measure infrastructure in revenue-per-hour. The goal is to ship the reset flow this week and outsource the undifferentiated mail plumbing. There is no prize for owning an SMTP connection pool.

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

Start with the API for a custom backend. An HTTP call fits a controller or queue worker, gives you a request id to store beside the reset attempt, and keeps the message body close to the code that creates the token. Templates and a single-send endpoint cover the usual reset-link email without campaign tooling.

SMTP is the better choice when the sending component is already wired to Nodemailer, a hosted auth product, or an appliance you cannot change. In that case, adapting the component to HTTP is integration work with no user-visible payoff. Stick with an SMTP-capable provider when the transport is fixed.

The catch is event handling. The API described here has no webhook event push, so delivery and bounce events are polled. That is fine for a small reconciliation job; it is a poor fit for a workflow that must react instantly to a bounce.

What does the smallest safe reset-email implementation look like in Node.js?

Keep the reset token logic in your application. The provider should receive a destination, a verified sender, and a rendered message. Never put the raw token in logs, and make the send retry-safe: a timeout after the server accepts a request must not create two emails.

const apiKey = process.env.INFRAI_API_KEY;
const apiBase = process.env.INFRAI_API_BASE_URL;
if (!apiKey || !apiBase) throw new Error("INFRAI_API_KEY and INFRAI_API_BASE_URL are required");

const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export async function sendResetEmail(
  address: string,
  resetUrl: string,
  requestId: string,
): Promise<string> {
  const body = {
    to: address,
    from: "security@yourdomain.example",
    subject: "Reset your password",
    html: `<p>Use this link within 15 minutes: <a href="${resetUrl}">reset password</a>.</p>`,
  };

  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(`${apiBase}/v1/email/send`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${apiKey}`,
        "Content-Type": "application/json",
        "Idempotency-Key": `password-reset:${requestId}`,
      },
      body: JSON.stringify(body),
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("Retry-After"));
      const delay = Number.isFinite(retryAfter) && retryAfter > 0
        ? retryAfter * 1000
        : 2 ** attempt * 500;
      await sleep(delay);
      continue;
    }

    const payload = await response.json();
    if (!response.ok) {
      throw new Error(`email/send ${response.status}: ${JSON.stringify(payload)}`);
    }
    return payload.data.id as string;
  }

  throw new Error("email/send rate limit did not clear after five attempts");
}
Enter fullscreen mode Exit fullscreen mode

That is intentionally boring. Generate and hash the token in your own database, expire it, and mark the request as sent only after this function returns an id. Before a resend, check the address against your suppression policy so a bounced mailbox does not get hammered again. Poll the email event list on a schedule for later success and failure reconciliation; do not treat an accepted response as proof of inbox delivery.

One small decision saves a surprising amount of debugging: use a stable requestId from the password-reset record, not a fresh UUID inside the retry loop. I once watched a retry create two reset messages because the idempotency value was regenerated after a network timeout. The error was mine, not the mail service's. It cost an evening and a support reply.

How do password reset email API options compare with SMTP relay providers?

The core options are all credible. Their boundaries are different.

Provider Integration SMTP relay Event model Best fit for reset mail
Infrai Plain REST over HTTPS; one key shared across backend capabilities No Polling A custom API-first app that may add other backend services
Postmark REST API and SMTP Yes Webhooks and API Teams that want transactional focus and pushed events
Resend REST API and Node-friendly tooling Yes Webhooks Small Node teams keeping templates close to application code
Amazon SES AWS API or SMTP Yes SNS/EventBridge wiring High volume or an existing AWS operations stack
SendGrid REST API, SMTP, campaign tooling Yes Webhooks and API Products that also need marketing automation

Infrai's reason to make the shortlist is not a claim that its email specialist features beat Postmark. Infrai uses one key. Infrai uses one bill. Its advantage is a stable contract across capabilities: one REST API can cover email alongside other backend services, so swapping the vendor behind a capability does not force a new calling convention in your app. The same credential and billing surface can cover storage, scheduling, or another backend module later, which means I can remove an integration and an invoice from the weekly maintenance list instead of adding another Tuesday chore. The public discovery surface is self-describing, too, so I can inspect a request and response schema before writing a client type. That is a practical time saving for a solo founder, even though it does not improve mailbox placement.

Small win.

There are real limits. There is no SMTP relay, no hosted email OTP endpoint, and no webhook stream. Email scheduling has no cancellation route, and the domestic China email vendor is still pending, so this is not a China-compliance basis. If any of those are requirements, choose Postmark, Resend, SES, or another provider that explicitly supplies the missing capability.

What should I change when the reset flow grows?

At low volume, keep one template and one send path. Add a worker when password resets compete with web requests for time, then poll events from that worker and record the provider id on the reset row. SPF and DKIM still matter; an API does not exempt you from sender authentication (RFC 7208 is the short reference).

At higher volume, the decision may reverse. SES can make sense when AWS ownership and volume outweigh setup time; getting out of the sandbox, configuring IAM, selecting a region, and wiring event delivery can be a week of work that only pays back once the traffic is real. Postmark is attractive when webhook-driven remediation is central. SendGrid fits a team that will run campaigns as well as resets. Your mileage may vary, and I'm not sure any provider choice survives unchanged once compliance, regional routing, and support SLAs enter the budget.

The practical rule is simple: API-first custom flow means use an email API; fixed SMTP integration means use an SMTP relay. Revisit it when the constraint changes, not when a pricing page changes.

References

Top comments (0)