@021.is/spine-email
v0.4.3
Published
Resend wrapper with structured logging, retries, and a uniform send API. Templates live in the consuming app.
Readme
@021.is/spine-email
Resend wrapper. Bearer auth, retry on 5xx, terminal on 4xx (no infinite-loop on permanent client errors), idempotency-key passthrough, structured logging hook.
Use
import { makeEmailClient } from "@021.is/spine-email";
const email = makeEmailClient({
apiKey: env.RESEND_API_KEY,
defaultFrom: "My App <[email protected]>",
retries: 2,
logger: console, // optional pino-compatible logger
});
await email.send({
to: "[email protected]",
subject: "Welcome to My App",
html: renderToString(<WelcomeEmail name="Sam" />),
tags: { app: "my-app", flow: "signup" },
idempotencyKey: `signup-${userId}`,
});Behavior
- 5xx → up to
retriesmore attempts (default 2) - 4xx → throws
SomethingWentWrongExceptionimmediately, no retries (4xx is permanent — looping wastes time + money) - Missing both
htmlandtext→ throws - Sets
idempotency-keyheader when provided (Resend dedupes server-side)
React Email templates
Templates live in the consuming app (React Email, MJML, plain HTML — your call). spine-email is just the transport.
Why not just fetch
Default fetch has no retry, no logging, no terminal-on-4xx logic, no idempotency. After the third app reimplemented it, it became a package.
