Short answer: for a password reset email, classify a malformed request, invalid sender domain, and template render error in one Node.js contract before sending. Validate the API JSON first, then render the signup verification link from a pinned template version.
There are four different failures: malformed JSON is an application input defect; an invalid From domain is configuration; a template render error is a content contract failure; mailbox filtering is downstream behavior. Each needs its own evidence. Log a request ID, field path, sender domain, and template version. Hash or redact the token.
This separation matters for a one-person SaaS. I once retried a sender rejection three times because it looked like a timeout. The queue stayed busy and nothing improved. Now permanent validation errors stop at the adapter, while transient network failures use bounded retries. Small distinction. It saves hours.
How should a password reset email handle a malformed request?
Start with a provider-neutral message: recipient, approved sender identity, subject, HTML, text, and request ID. Check the exact sender allow-list, not a loose suffix. DKIM authenticates a signing domain; it does not make every visible From address valid (RFC 6376). Rendering tests should include an ampersand, a non-ASCII name, and a URL containing &, then verify both bodies contain the same HTTPS verification URL.
type VerificationMessage = {
requestId: string;
to: string;
from: { email: string; name: string };
subject: string;
html: string;
text: string;
};
function makePayload(message: VerificationMessage) {
if (!message.to.includes("@")) throw new Error("recipient must be an email");
if (!message.from.email.endsWith("@mail.example")) {
throw new Error("sender domain is not approved");
}
if (!message.html.includes("/verify")) {
throw new Error("verification link is missing");
}
return {
personalizations: [{ to: [{ email: message.to }] }],
from: message.from,
subject: message.subject,
content: [
{ type: "text/plain", value: message.text },
{ type: "text/html", value: message.html },
],
headers: { "X-Request-Id": message.requestId },
};
}
const payload = makePayload({
requestId: "signup-8f31",
to: "new-user@example.net",
from: { email: "no-reply@mail.example", name: "Acme" },
subject: "Verify your account",
html: '<p>Verify <a href="https://app.example/verify?t=opaque-token">your account</a>.</p>',
text: "Verify your account: https://app.example/verify?t=opaque-token",
});
await fetch("https://api.example.test/v1/email/batch/send", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
Pin the real API schema in a contract test. Syntactically valid JSON can still contain the wrong array or object type. Record the response class and request ID, never the rendered body or token. Keep template versions immutable and store the version beside the signup event. TOTP can support recovery, but RFC 6238 still requires an explicit time window and replay policy.
Run one fixture in this order: parse JSON, validate the sender, render hostile variables, inspect the URL host and expiry, then deliver to a controlled mailbox. Keep counters for rejected inputs, render failures, accepted API requests, and mailbox outcomes. Alert on ratio changes, not a single bad address. Your mileage may vary because recipient policies differ, and I'm not sure a synthetic mailbox models every rule. For a release review, I attach the fixture output, template version, domain configuration, and adapter request ID to one change record; that lets a solo maintainer reconstruct the path six months later without searching message bodies or asking a provider to explain an opaque dashboard state.
A deployment should fail when the approved domain, signing selector, or referenced template version is absent. That is easier to diagnose than tracing a link through a queue and a provider dashboard while a new account waits.
When is delegated template ownership the better trade?
| Choice | Keep templates in the repo | Use a managed editor |
|---|---|---|
| Best fit | Product copy and atomic releases | Frequent non-engineering edits |
| Earliest test | Rendered HTML and link semantics | Payload and template-ID drift |
| Main responsibility | Escaping, accessibility, rollback | Adapter schema and external versioning |
Own the template when its wording and verification flow are product behavior. Delegate it when localization or daily copy edits make code review the bottleneck. The catch is control: an editor becomes another deployed dependency. Keep the sender allow-list, expiry calculation, and verification URL rules in application code. Repository ownership is not suitable when nobody can review HTML; managed editing is not suitable when rollback must be atomic.
I ship weekly, so I outsource undifferentiated plumbing while keeping the contract customers depend on. The choice is failure containment, not a headline price.
Top comments (0)