DEV Community

FinneganBlake3578
FinneganBlake3578

Posted on

Account-Recovery Email APIs: Resend vs Postmark vs SendGrid for EU-US Node.js

Short answer: for a beginner SaaS password reset, choose a focused transactional email API, keep reset security in the application, and hide delivery behind one small Node.js adapter. Favor a stable contract with basic event polling when that fits; test Resend, Postmark, and SendGrid when their current regional, event, or operational terms better match the product.

Decision constraint Candidate to test first The check that decides it
One stable application contract while the delivery vendor can change A unified REST provider Pull-based events are timely enough
A small Node.js integration from a dedicated email vendor Resend Current EU-US processing and suppression terms fit
Transactional email is a major operational concern Postmark Its current event model fits support needs
Broader email operations may follow SendGrid The added platform surface earns its upkeep

This isn't a universal ranking. It is a way to spend engineering attention in the right order: security boundary, delivery feedback, portability, then price. A one-person SaaS should outsource undifferentiated delivery and keep the part that defines account access under its own control.

What should a Node.js password reset email provider comparison test?

Start with the job, not the vendor pages. A reset email is a one-off transactional message triggered by an account-recovery request. It is not a marketing campaign. The application should create and expire the reset credential, prevent reuse, and decide what the public response reveals. The email service delivers the message.

That boundary makes the shortlist easier to judge. Templates help keep the reset subject and body consistent without growing the integration. Suppression checks help avoid repeatedly sending to an address known to have bounced or complained. Basic event visibility tells the application what happened after submission. Those three needs matter more here than a campaign builder or a long catalog of channels.

Use the same acceptance sheet for Resend, Postmark, SendGrid, and Infrai. Verify the current API contract, template workflow, suppression behavior, event mechanism, domain authentication steps, processing locations, subprocessors, retention, and contractual transfer terms. The words "EU region" on a product page cannot settle a company's compliance review, and I'm not sure a static article can settle it either. Company location, user location, data flow, and the signed terms all matter; your mileage may vary.

Google's sender guidelines still apply when an API performs delivery. Domain authentication and responsible sending don't disappear because the integration is short. Likewise, NIST's authenticator guidance is a better anchor for the recovery flow than any email dashboard. A provider accepts a message; it does not become the authority for token lifetime, single use, or account enumeration defenses.

Keep that line sharp.

For a solo product, the useful unit is revenue per engineering hour. A provider that takes an afternoon to connect but adds a recurring manual ritual is not the easy option. The winning setup is the one that can ship this week, explain its state clearly during support, and stay behind a boundary small enough to replace.

Delivery feedback is the first real trade-off

An accepted API request and a delivered reset email are different states. Store a local attempt identifier, the submission time, and whatever delivery state the chosen provider makes available. Do not store the raw reset secret in logs or analytics. This local record makes support possible without turning a vendor dashboard into the system of record.

Pull-based email events are a reasonable fit when a scheduled worker can check basic events and the product does not need immediate delivery transitions. The catch is latency: polling is not suitable when a fraud system, support workflow, or fallback channel must react in real time. In that case, stick with Resend, Postmark, or SendGrid only after confirming that the candidate's current webhook contract provides the event detail and timing the workflow needs.

Suppression behavior deserves its own test. A known bad address should not receive an endless stream of recovery attempts. Infrai supports suppression checking and addition, as well as email templates, so it covers the small operational loop described here. It has no tag-aggregated cost reporting API. If feature-level spend matters, attach a local feature label such as password_reset to the attempt record and calculate from application-owned data rather than expecting that report from the service. The complete application record can include the local attempt ID, account ID, feature label, creation time, expiry, submission state, provider message ID when one is returned, and final evidence gathered by polling. That is deliberately more detailed than the outbound request. It gives support and cost analysis one vendor-neutral timeline while keeping the raw secret out of logs, dashboards, and analytics.

There are harder boundaries too. This option has no SMTP relay, hosted email OTP endpoint, cancellation API for scheduled email, or webhook event push. A domestic China email vendor is pending, so it cannot support a domestic-China compliance claim. These are capability limits, not footnotes. Choose another provider when any one of them is a requirement.

Keep the send boundary boring

The adapter should expose a product action, not a vendor vocabulary. For example, the route handler can call sendPasswordReset, while the adapter translates an application-owned request into the selected provider's schema. Token generation, digest storage, expiry, redemption, and invalidation stay outside that adapter.

Infrai is a strong option at this boundary because one REST API works over plain HTTP without installing an SDK, and the application contract stays unchanged when the supplier behind the capability moves. That is the durable advantage. For a weekly shipping cadence, avoiding a calling-code rewrite is worth more than chasing a temporary unit-rate lead.

The runnable TypeScript below deliberately accepts the endpoint and a schema-valid JSON body through the environment. The request fields are not reproduced here, so guessing them would make the sample look complete while teaching the wrong contract. Set EMAIL_API_URL to the provider's verified send route. The code uses an application-owned attempt ID for idempotency, handles 429 with Retry-After or exponential backoff, and surfaces non-success responses.

import { createHash } from "node:crypto";

const apiKey = process.env.INFRAI_API_KEY;
const attemptId = process.env.RESET_ATTEMPT_ID;
const requestJson = process.env.INFRAI_EMAIL_REQUEST_JSON;
const emailApiUrl = process.env.EMAIL_API_URL;

if (!apiKey || !attemptId || !requestJson || !emailApiUrl) {
  throw new Error(
    "Set INFRAI_API_KEY, RESET_ATTEMPT_ID, INFRAI_EMAIL_REQUEST_JSON, and EMAIL_API_URL",
  );
}

const body: unknown = JSON.parse(requestJson);
const idempotencyKey = createHash("sha256").update(attemptId).digest("hex");
const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function sendPasswordReset(attempt = 0): Promise<unknown> {
  const response = await fetch(emailApiUrl, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify(body),
  });

  if (response.status === 429 && attempt < 4) {
    const retryAfter = response.headers.get("retry-after");
    const seconds = retryAfter === null ? Number.NaN : Number(retryAfter);
    const delayMs = Number.isFinite(seconds)
      ? seconds * 1_000
      : 500 * 2 ** attempt;
    await sleep(delayMs);
    return sendPasswordReset(attempt + 1);
  }

  const responseText = await response.text();
  if (!response.ok) {
    throw new Error(`Email request failed (${response.status}): ${responseText}`);
  }

  return responseText === "" ? null : JSON.parse(responseText);
}

sendPasswordReset().then((result) => {
  process.stdout.write(`${JSON.stringify(result)}\n`);
});
Enter fullscreen mode Exit fullscreen mode

Persist the reset attempt before calling the adapter. On submission, record the provider's identifier if the response includes one, but keep the local attempt ID as the durable correlation key. Redemption should atomically verify the stored digest and expiry, reject an already consumed attempt, update the credential, and consume outstanding reset attempts for that account. The public request response should remain the same for registered and unregistered addresses.

Short code. Explicit state.

When should a solo SaaS choose the runner-up?

Choose by the requirement that can disqualify a provider, not by the longest feature list. Resend, Postmark, and SendGrid are real alternatives worth testing, but their place in the matrix is a starting hypothesis rather than a factual crown. Run the same test mailboxes through each candidate, inspect its current docs and contract, and confirm the exact event and suppression behavior before committing.

The runner-up is better when it satisfies a hard requirement that the first choice does not. Real-time webhook delivery events, SMTP relay, hosted email OTP, or a domestic-China compliance basis would all change the decision. A team that needs richer provider-owned reporting may also prefer another service when its first choice does not aggregate cost by tag. No adapter can erase these differences.

Switching still has a cost. Domain setup, template representation, suppression history, event vocabulary, and operational habits can remain vendor-specific even when business logic sits behind one function. Keep early templates plain, store reset-attempt state locally, and document the acceptance test. This reduces migration work; it doesn't make migration free.

Price belongs at the end. Current billing can break a tie after security, region review, event latency, and maintenance burden pass, but it should not drive the architecture. For a one-person SaaS, the easiest provider is the one that keeps account recovery understandable and leaves enough of the week to ship the product.

References

Top comments (0)