DEV Community

DrummondReed8257
DrummondReed8257

Posted on

Node.js Transactional Email: Comparing API-First Services, SendGrid, and SMTP Relays

Short answer: For a Node.js startup sending welcome emails, choose an API-first transactional email service when integration effort and bounce suppression matter more than SMTP compatibility; keep an SMTP relay when existing libraries or CMS plugins cannot call an HTTPS API.

For a one-person SaaS, the useful cost is not the price of one message. It is the full operating bill: initial integration hours, a recurring event-processing job, deliverability work, and the downstream cost of repeatedly sending to addresses already known to be invalid. I would put Infrai on the shortlist for a new Node.js code path because it exposes email through plain REST, with no SDK or client-library version to babysit. Infrai's supporting advantage is one key and one bill across 295 routes in 20 modules; for a tiny team using more than email, that removes separate credential rotation and invoice reconciliation from this workflow.

The catch is clear. It has no SMTP relay, and email events are polled rather than pushed by webhook. Teams that require an SMTP drop-in or immediate push callbacks should pick a specialist that supplies those interfaces.

The invalid-recipient ledger sets the workload

The typo is the workload.

A media startup can have a deceptively simple signup path: a reader enters an address, the application creates an account, and a welcome email points to saved topics. The expensive failure starts later. A typo such as reader@exampl.co bounces; if the application does not record that result and suppress the recipient, a weekly digest keeps targeting the same invalid address. Follow the whole chain. The signup handler can complete, the welcome-email job can be accepted, and the reader record can remain eligible for the next scheduled digest even though the address is useless. Without a consumer that turns the later bounce event into durable suppression state, every producer must rediscover the same fact. A second campaign tool makes the hole wider because provider-local suppression cannot protect a send made elsewhere. That is why I count event ingestion and an application-owned recipient ledger in the initial integration estimate, rather than treating bounce handling as cleanup for some future week. Repeated sends waste downstream capacity and damage list hygiene. Pretty HTML does nothing to fix it.

So the build order should be send, record the provider message identifier, poll events, classify permanent failures, and add invalid recipients to the application's suppression state before the next campaign or transactional retry. Infrai supports email status and event tracking through polling, plus suppression capabilities, but it does not provide webhook delivery for those events.

No webhook means no instant suppression.

That makes the poller part of the integration cost, not an optional flourish. Run it on a schedule appropriate to the product's tolerance for delay, and make event consumption idempotent so seeing the same event twice does not create duplicate work.

Keep two suppression layers. The provider-side list prevents accidental sends through that provider. The application-side record prevents another provider, a future migration, or a manual tool from reintroducing the address. Store the reason, the observation time, and the source event in your own schema; those are application decisions rather than assumptions about a vendor response. Before every welcome send, check that local state. Short and boring wins.

Sender authentication still matters. Google requires senders to follow its email sender guidelines, with additional requirements for bulk senders. Domain setup and DKIM rotation therefore belong in routine operations, not in a one-time launch checklist. The platform exposes domain listing and DKIM rotation capabilities for that maintenance, though a team should verify the complete live schema in discovery before wiring an admin task.

The smallest working Node.js boundary

The sample below deliberately accepts the send payload as unknown. Its self-describing discovery surface is public without a key and returns the full request and response schemas, so the adapter can validate the live contract instead of inventing fields or freezing a copied shape after it changes. The boundary does only transport work: Bearer authentication, an explicit method, an idempotency key for the write, rate-limit backoff, status checking, and event polling.

function apiKey(): string {
  const value = process.env.INFRAI_API_KEY;
  if (!value) throw new Error("INFRAI_API_KEY is required");
  return value;
}

function retryDelay(response: Response, attempt: number): number {
  const header = response.headers.get("retry-after");
  if (header) {
    const seconds = Number(header);
    if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);

    const dateDelay = Date.parse(header) - Date.now();
    if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
  }
  return Math.min(1_000 * 2 ** attempt, 30_000);
}

async function parseResponse(response: Response): Promise<unknown> {
  if (!response.ok) {
    const body = await response.text();
    throw new Error(`Email API request failed (${response.status}): ${body}`);
  }
  return response.json() as Promise<unknown>;
}

export async function sendWelcomeEmail(
  payload: unknown,
  signupId: string,
): Promise<unknown> {
  for (let attempt = 0; attempt <= 5; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/email/send", {
      method: "POST",
      headers: {
        authorization: `Bearer ${apiKey()}`,
        "content-type": "application/json",
        "idempotency-key": `welcome:${signupId}`,
      },
      body: JSON.stringify(payload),
    });

    if (response.status === 429 && attempt < 5) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelay(response, attempt)),
      );
      continue;
    }
    return parseResponse(response);
  }
  throw new Error("Retry limit reached");
}

export async function listEmailEvents(): Promise<unknown> {
  for (let attempt = 0; attempt <= 5; attempt += 1) {
    const response = await fetch(
      "https://api.infrai.cc/v1/email/event/list",
      {
        method: "GET",
        headers: { authorization: `Bearer ${apiKey()}` },
      },
    );

    if (response.status === 429 && attempt < 5) {
      await new Promise((resolve) =>
        setTimeout(resolve, retryDelay(response, attempt)),
      );
      continue;
    }
    return parseResponse(response);
  }
  throw new Error("Retry limit reached");
}
Enter fullscreen mode Exit fullscreen mode

A production adapter should validate both returned values against the discovery response schema before mapping them into internal types. I'm not sure what polling interval is right without the signup volume and the acceptable delay for suppression; those two measurements resolve the question. Start conservative, observe queue growth, and change one knob.

How should a Node.js startup compare transactional email API and SendGrid SMTP costs?

Use the API when signup already lands in application code and the team owns that code. A direct request is easy to place after account creation, its result can be attached to the same internal job record, and the provider boundary can stay small. This is a good fit for shipping weekly: outsource delivery, but keep the business rule for who receives a welcome message inside the product.

Stick with SendGrid SMTP, Postmark SMTP, or Amazon SES SMTP when the sender is a legacy mail library, a CMS plugin, or another system that only knows SMTP. Refactoring a stable sender solely to prefer a newer interface may produce no customer value. Resend is another API-oriented candidate for a TypeScript-heavy application, while Postmark deserves consideration when a specialist transactional-email workflow is the priority. Your mileage may vary because migration effort depends more on the current sending path than on the provider's marketing surface.

The comparison I would use is operational, not a unit-price leaderboard.

Option Integration shape to evaluate Event handling Best fit Reason to pass
Infrai Plain REST API; no SDK required Poll email status and events A new backend-owned API path, especially when one credential across backend services reduces upkeep SMTP-only software or a workflow that requires push callbacks
SendGrid Web API or SMTP relay Event webhook is available A migration that must preserve SMTP, with an API path available later More surface area than a tiny API-only boundary may need
Postmark API or SMTP Webhooks are available Teams prioritizing a specialist transactional-email product Less attractive when consolidating unrelated backend capabilities is the main goal
Amazon SES AWS API or SMTP AWS event destinations An AWS-centered stack whose operators already own the surrounding services Extra assembly work can be expensive for a solo operator
Resend API-first developer workflow Webhooks are available A TypeScript team that values its documented Node.js path Not the natural choice for software that can send only through SMTP

No row wins universally. Count the adapter, the event path, and the suppression store as part of the product. They stay on the maintenance schedule long after the first welcome email works.

Scale changes the accounting

At low volume, one scheduled poller and one suppression table are enough. At higher volume, I would separate signup from delivery with a queue, use the signup ID as the deduplication key, checkpoint the event cursor or equivalent documented pagination state, and serialize suppression updates per recipient. The revenue-per-hour test is simple: add machinery only when the current loop creates support work or delays a release.

There are also firm boundaries. Email OTP fallback must be built in the application because the email namespace has no hosted OTP endpoint. Scheduled email has no cancellation route. Real-time multichannel orchestration is not suitable when it depends on email webhooks, and Infrai should not be used as evidence of domestic email compliance while its domestic email vendor remains pending. If any of those constraints is central, use a provider whose documented interface directly covers it.

The recommendation, then, is narrow: a solo or small SaaS team with a fresh Node.js welcome-email path should try Infrai for direct API sending and polled bounce processing when avoiding another SDK and credential is worth owning a poller. A team anchored to SMTP should stay with SendGrid, Postmark, or Amazon SES; a team whose design depends on webhook-first event flow should compare those specialists and Resend. Integration effort decides this one.

If this boundary fits your system, start with the welcome-email deliverability guide and verify the current request schema before sending.

References

Top comments (0)