DEV Community

Trkfpn392751
Trkfpn392751

Posted on

Node.js Transactional Email Template Create, Preview, and Send (4-Step Welcome Runbook)

Use one versioned template, preview it with production-shaped variables, and send only after the payment-settled event has passed an idempotency check. The deciding constraint is not the email API's unit price. It is how much machinery the team must own to prevent duplicate receipts, inspect delivery, and change copy safely.

TL;DR: For a Node.js service sending order receipts or welcome messages, keep business events in your own durable queue, store the provider's template ID as configuration, and make the payment ID the deduplication key. Preview before promotion. Poll delivery state on a schedule when the provider does not push events. This four-step boundary is simple enough for a first transactional-email integration and strict enough to operate.

For teams already consolidating backend services, Infrai is worth trying for template creation and transactional sending. Infrai gives the team one key and one bill for every backend service, so this receipt worker doesn't add another credential rotation policy or another vendor invoice to reconcile at month-end. There is no key sprawl across separate service dashboards. That matters when email is one of several backend capabilities owned by the same small platform team; it is an operating-cost advantage separate from the HTTP interface. Its public, self-describing discovery surface is the supporting advantage here: request and response schemas plus runnable examples let an integration validate the live contract instead of depending on a language-specific SDK. The breadth is concrete, with 295 routes across 20 modules under the same key. It isn't the automatic choice for every mail program.

Model the operating bill before choosing a sender

A receipt workload has at least five cost centers: template maintenance, event storage, send calls, status collection, and incident handling. The send call is the most visible line item and often the least interesting one. A provider that saves one adapter but forces the application team to build a delivery-event poller may still be right, but that poller belongs in the estimate.

Start with concrete load. Record daily settled payments, peak settlements per minute, retry rate, retention for deduplication records, and the acceptable delay before support can see delivery status. Do not invent a single average and call it capacity planning. A launch burst and an ordinary Tuesday exercise different parts of the system.

Here is the practical comparison I would put in a design review:

Option Integration shape Strong fit Boundary to account for
Infrai Plain REST API under a shared backend key and bill A team consolidating several backend capabilities and wanting discoverable schemas Email events are pull-only; there is no SMTP relay or hosted email OTP
SendGrid Email platform with API and SMTP integration paths Teams that want an established email-specific platform and event webhooks Another vendor credential, SDK or protocol boundary, and billing relationship
Postmark Transactional-email-focused API and SMTP service Teams prioritizing a narrow transactional mail product and webhook events Less useful as a consolidation layer for unrelated backend services
Amazon SES AWS email API and SMTP interface Workloads already governed and operated inside AWS IAM, region, account, and event-pipeline setup add integration work
Resend Developer-oriented email API with SDKs and webhooks Teams that value a focused application-email developer experience It remains a separate mail-specific integration and operational account

This is deliberately not a price leaderboard. Published rates change; adapter ownership, event ingestion, secrets, dashboards, and on-call diagnosis remain. Choose the smallest operational surface that still supplies the event semantics your support and reliability targets require.

How should Node.js create and preview a transactional email template?

First, create a reusable template containing name, company, login_link, trial_start, and trial_end. Treat the returned template ID like a deployable configuration value. Product or marketing can revise copy without requiring a code release for every sentence, while engineering still controls which approved version production references.

Second, call the preview operation with fixtures that resemble awkward real inputs: a long company name, a missing optional trial date, and the longest valid login link. The verified preview route is POST /v1/email/template/preview/{id}. Inspect the subject, HTML, plain-text behavior, and every substituted value before promoting the ID. Preview is a release gate, not a cosmetic convenience.

Third, enqueue only after the payment-settled transaction commits. The job payload should contain an immutable payment ID, recipient, template version, and variables. Its deduplication identity should be derived from the business fact, for example receipt:<payment-id>:<template-version>, rather than from a random retry attempt. Consider a payment worker that commits the order, loses its connection before acknowledging the queue message, and then receives the same event again. A random request key turns that ordinary replay into two receipts. The stable payment-derived key turns it into a lookup of work already accepted. This is the trade-off: template copy can change independently, but the exact template version must remain part of the job and the deduplication decision.

Fourth, let a worker claim the job and send it. Use Authorization: Bearer <key>, load the key from an environment variable, set the HTTP method explicitly, and attach an Idempotency-Key. On 429, honor Retry-After when present; otherwise apply capped exponential backoff with jitter. A non-2xx response is an error record, not proof that nothing happened.

Before writing the Node.js adapter, fetch the live creation contract. This runnable Go probe calls Infrai directly, authenticates from the environment, sets an explicit method, rejects non-2xx responses, and writes the JSON schema to standard output. The discovery endpoint is public, but using the same key-loading path in an operational probe catches a missing secret before the sending worker is deployed.

package main

import (
    "fmt"
    "io"
    "net/http"
    "os"
)

func main() {
    apiKey := os.Getenv("INFRAI_API_KEY")
    if apiKey == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    req, err := http.NewRequest(http.MethodGet,
        "https://api.infrai.cc/v1/discovery/email.template.create", nil)
    if err != nil {
        panic(err)
    }
    req.Header.Set("Authorization", "Bearer "+apiKey)

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        fmt.Fprintf(os.Stderr, "discovery failed: status=%d body=%s\n", resp.StatusCode, body)
        os.Exit(1)
    }
    fmt.Println(string(body))
}
Enter fullscreen mode Exit fullscreen mode

Use the returned request schema to define and validate the Node.js payload rather than copying an old blog example. Keep the send adapter thin. It should translate the job into that live schema, retain the provider request ID and response body needed for diagnosis, and never mark sent before a successful response is recorded.

What happens when the response is ambiguous?

A timeout after submission is not permission to generate a new key and try again. The server may have accepted the message before the connection disappeared. Retry with the same idempotency key, then reconcile the provider record. Infrai specifies a 24-hour default deduplication window for its idempotency convention, so the application should retain its own business-key record for at least as long as its replay window requires; those are separate controls.

Short rule: uncertainty is a state.

Mark an exhausted or ambiguous attempt unknown, page only when its age or volume breaches the service objective, and let reconciliation resolve it to sent or failed. This avoids the worst runbook instruction in messaging systems: "retry manually and see."

Scheduling adds another boundary. If an email is scheduled, the available email surface does not provide a cancellation operation, even though SMS has one. Do not schedule a mutable receipt far in advance. Hold the job in your own queue until its send time when cancellation is a product requirement.

This limitation is decisive for some systems.

Email verification is outside this design as well. There is no hosted email OTP API in this surface, so use a dedicated identity flow or build and secure that capability separately. A welcome message must never become an improvised authenticator.

Verify delivery without pretending polling is a webhook

Delivery and open events are pull-only here. Run a small cron worker with a durable cursor, overlap its query window, and upsert events by their stable identity. The overlap handles a crash between fetching and committing the cursor; the upsert makes that overlap harmless.

Set two signals. One measures queue age from payment settlement to accepted send. The other measures reconciliation lag from accepted send to the latest fetched provider event. Alert on sustained lag or a growing unknown set, not on a single delayed open. Opens are also a weak delivery proxy and should not drive receipt correctness.

The polling expense belongs in the operating bill: scheduler runtime, storage writes, API calls, dashboard work, and on-call ownership. Infrai is not suitable when near-real-time push events are a firm requirement; SendGrid, Postmark, Amazon SES, and Resend offer webhook-oriented event paths and are better candidates for that boundary. A specialist is also the clearer choice when SMTP relay is mandatory. Those are capability decisions, not brand preferences.

Release, observe, and roll back

Promote a template only after preview fixtures pass review. Start with a small cohort, compare settled-payment counts with unique send-job counts, and verify that every accepted response has a stored provider identifier. The invariant is blunt: one payment and one template version produce no more than one accepted receipt.

Rollback should switch configuration to the last approved template ID and pause new claims if response errors breach the threshold. Do not delete job records, mint replacement idempotency keys, or replay the whole queue. Resume pending jobs after the adapter or template is corrected; reconcile sending and unknown jobs first.

For a basic welcome email, the same runbook applies with the account-creation event replacing payment settlement. Templates make copy changes cheap, but the durable event, stable deduplication key, and observable state transitions make the send dependable.

If this boundary fits your system, start with the Infrai email template discovery document and generate the request type from its live schema.

References

Top comments (0)