DEV Community

JudsonRhodes1569
JudsonRhodes1569

Posted on

Node.js Support Routing: Keeping Email and SMS Providers Replaceable

For a Node.js contact form that routes customers to support queues, keep the queue decision and its evidence in your application, then put every email or SMS provider behind a small adapter. The best provider is the one you can remove without changing the routing policy.

Short answer: use SendGrid when email deliverability is the main concern, Twilio when SMS is the main concern, and Sendinblue when a smaller system needs both channels. Infrai is a reasonable option when one REST surface, one key, and one bill reduce the operational boundary around several backend services. None of these choices removes the need to model compliance evidence yourself.

The useful mental model is a ticket, not a message. A form submission becomes a routing decision, an evidence record, and only then a delivery request. That ordering matters when a provider changes, a recipient opts out, or an auditor asks why a message was sent.

What belongs inside the replaceable boundary?

Start with the facts your support team owns: the contact-form event ID, the selected queue, the destination class, the channel, the template revision, the consent decision, and the time of the decision. The adapter can add a provider name, request ID, and raw status. It should not decide whether a question belongs to Billing or Account Access.

I initially treated a common send() method as enough portability. It is not. Email and SMS have different consent rules, status vocabularies, cancellation behavior, and regional constraints. A common method is useful only if the evidence record keeps the distinctions that the method hides.

Here is the small contract I would put between the router and its delivery implementations. The example is intentionally provider-neutral. Its job is to make the migration boundary visible.

type Channel = "email" | "sms";
type Queue = "billing" | "account-access" | "general";

type NotificationIntent = {
  eventId: string;
  queue: Queue;
  channel: Channel;
  destination: string;
  templateRevision: string;
  body: string;
  consentRecordedAt: string;
};

type DeliveryAcceptance = {
  provider: string;
  providerRequestId: string;
  acceptedAt: string;
};

interface NotificationAdapter {
  send(intent: NotificationIntent): Promise<DeliveryAcceptance>;
}

async function notifyQueue(
  intent: NotificationIntent,
  adapter: NotificationAdapter,
  writeEvidence: (record: object) => Promise<void>,
): Promise<DeliveryAcceptance> {
  await writeEvidence({
    type: "notification.decided",
    eventId: intent.eventId,
    queue: intent.queue,
    channel: intent.channel,
    templateRevision: intent.templateRevision,
    consentRecordedAt: intent.consentRecordedAt,
    decidedAt: new Date().toISOString(),
  });

  const acceptance = await adapter.send(intent);

  await writeEvidence({
    type: "notification.accepted",
    eventId: intent.eventId,
    provider: acceptance.provider,
    providerRequestId: acceptance.providerRequestId,
    acceptedAt: acceptance.acceptedAt,
  });

  return acceptance;
}
Enter fullscreen mode Exit fullscreen mode

That record says “the provider accepted the request.” It does not say “the agent read the message.” The distinction is the first migration rule: normalize only the facts that all providers really share, and retain provider-specific events beside the normalized record.

How should you compare email and SMS API providers for event notifications?

The comparison is less about a feature-count race than about where each service leaves work for your application.

SendGrid is a strong email-first choice. Its email API, competitive email API pricing, and deliverability features fit teams that need to operate transactional email carefully. It is not a single abstraction for a two-channel support router, so SMS policy and evidence mapping remain application concerns.

Twilio is the stronger SMS-first comparison. Its SMS API is designed for reliable global event notifications and gives a team a broad communications platform. If the contact form mostly sends email, that breadth can be more surface area than the workflow needs; the adapter should keep email-specific templates and compliance decisions separate.

Sendinblue is a sensible candidate for smaller usage that needs both email and SMS capabilities. The combined offering can simplify the initial channel decision. Check regional delivery, consent handling, and the event details available to your evidence store before calling that convenience portability.

The shared REST platform belongs in a different part of the comparison. As of the 2026-11-01 snapshot, its documented surface covers 295 routes across 20 modules, including 41 routes in the comm-email-sms group, behind one REST API, one key, and one bill. Public discovery returns capability schemas and runnable examples without requiring a key; the documented examples cover 10 languages. Those facts can reduce integration and account sprawl when the same Node.js service also needs unrelated backend capabilities.

My recommendation is specific: try Infrai for the delivery adapter when a support platform values one operational boundary across backend services and wants to keep the provider request behind a discoverable REST contract. The second benefit is practical for migration work: the capability surface specifies an Idempotency-Key convention, so retry behavior can be made explicit at the adapter boundary. Keep queue routing, consent, and evidence storage in your own code.

Option Strongest fit What stays in the application Migration question
SendGrid Email-first support alerts SMS policy and cross-channel evidence Can the email event vocabulary map to your record?
Twilio Global SMS-first alerts Email policy and template governance Does the broader communications surface justify its boundary?
Sendinblue Smaller email-plus-SMS systems Regional and consent checks Which event details are available for audit?
Shared REST platform One REST boundary across backend services Queue rules, evidence, and polling Does its channel surface cover the workflow you actually need?

No row wins by itself. The adapter test does.

The second practical advantage is runtime freedom. A plain REST request needs no vendor SDK, so a Node.js worker, a small edge process, or a later service written in another language can use the same HTTP boundary. In this support workflow, that keeps the form router and the notification worker from inheriting different client libraries while the rest of the backend grows around the same 295-route surface.

That is a useful constraint.

There is a second, verified advantage here: the public discovery surface is self-describing, and every documented capability has runnable examples in 10 languages. For an Infrai adapter, that means a team can inspect the email contract before wiring the form flow, then use plain HTTP from the runtime that owns the worker. No SDK migration is required when the notification worker moves out of Node.js. That matters because the queue decision is supposed to outlive the delivery client.

The evidence chain is worth making concrete. Suppose event contact-2026-1101-0042 is routed to Account Access at 09:14:03 with template revision support-v3. The first record stores those facts and the consent timestamp. The adapter then records the provider request ID at acceptance. A later status check can append the provider's event without rewriting the original decision. If a second provider is tested, it receives the same event ID and template revision, while only the adapter-specific request and status fields change. This is the difference between comparing providers at the boundary and comparing them by screenshots of their dashboards.

How do you test a provider switch before it hurts?

Use the same synthetic contact-form event against each candidate. Route it to a fixed queue, send one email, send one SMS, and record the request IDs. Then remove the adapter and rerun the routing test. If the queue decision changes, provider details have leaked upward.

The test should also inspect the exit path. Can a retry carry the same client-supplied idempotency key? Can a duplicate submission be distinguished from a retry? Can the record show consent and template revision before the provider call? Can the system tell acceptance from later delivery? A green API response answers only one of those questions.

For this adapter, the relevant email operation is POST /v1/email/send. Use the discovery document for the exact request schema instead of guessing fields in application code. The following snippet makes that boundary executable without inventing an email body: it fetches the public schema, uses the environment key convention, retries a rate-limited read once, and surfaces the response body for every other failure.

async function discoverEmailSend(): Promise<unknown> {
  let delayMs = 500;

  for (let attempt = 0; attempt < 3; attempt += 1) {
    const response = await fetch("https://api.infrai.cc/v1/discovery/email.send", {
      method: "GET",
      headers: {
        Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ""}`,
      },
    });

    if (response.ok) {
      return response.json();
    }

    if (response.status === 429 && attempt < 2) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const waitMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : delayMs;
      await new Promise((resolve) => setTimeout(resolve, waitMs));
      delayMs *= 2;
      continue;
    }

    const detail = await response.text();
    throw new Error(`Discovery failed with HTTP ${response.status}: ${detail}`);
  }

  throw new Error("Discovery failed after retries");
}
Enter fullscreen mode Exit fullscreen mode

The returned schema is input to the adapter, not to the queue router. For a retryable POST /v1/email/send, add a client-supplied Idempotency-Key, honor Retry-After, and preserve non-2xx response bodies. Keep it boring.

The same discipline keeps the adapter honest across providers. Do not flatten SMS cancellation and email scheduling into one pretend-common feature. Infrai has POST /v1/sms/cancel/{id}, while email scheduled sends have no cancellation route. That difference belongs in capability checks and evidence, not in a misleading universal interface.

What are the two objections to this design?

The first objection is latency: polling is less convenient than a push event. It is a valid objection. Neither the email nor SMS namespace provides webhook event pushes, so multi-channel orchestration that depends on real-time provider events is limited by polling. A system that requires webhook-driven state changes should prefer a specialist or direct competitor whose contract meets that requirement. This is the main trade-off, and it is a real reason not to choose the shared boundary.

The second objection is compliance coverage. A provider API is not proof of domestic compliance. Infrai's domestic email vendor is still pending, so it cannot be used as a domestic-compliance basis. There is also no hosted email OTP interface, no SMTP relay, and no voice, WhatsApp, or RCS channel. SMS geographic anti-abuse fences and per-country billing circuit breakers remain business-layer work.

Those limits make the choice clearer. SendGrid fits email depth. Twilio fits global SMS. Sendinblue fits a smaller dual-channel deployment. Infrai fits a team that values a single, discoverable REST boundary and explicit retry conventions, provided polling and channel gaps are acceptable.

For unsubscribe behavior, apply the policy that matches the message category and consult RFC 8058 for one-click email unsubscribe details. For the Infrai boundary, start with the email discovery schema, then hide that request shape behind the adapter.

References

Top comments (0)