Send transactional emails with React Email templates, recipient-locale rendering, and pluggable delivery adapters.
VitNode provides a unified transactional email service accessible via c.get("email"). Emails are rendered using React components, localized to the recipient's language, and delivered through your chosen provider.
Quick start
Once an adapter is configured, any route can email the signed-in member:
import { HTTPException } from "hono/http-exception";
import { buildRoute } from "@/api/builders";
export const sendWelcomeRoute = buildRoute({
route: {
method: "post",
path: "/",
responses: { 202: { description: "The email is queued." } },
},
handler: async c => {
const user = c.get("user");
if (!user) throw new HTTPException(401);
await c.get("email").send({
user, // their address and language
subject: "Welcome to VitNode!",
content: () => "Thank you for joining our community.",
});
return c.body(null, 202);
},
});send() renders the email during the request and queues it. It is handed to the provider right after the response goes out, so a sign-up link does not wait for the next cron run, and the queue retries it if that attempt fails. Answer 202, not "sent", because the provider has not seen the email yet when the route returns.
For a translated email with the standard layout, see Templates.
Deliver immediately
build() followed by deliver() waits for the provider during the request. Use it only when the route needs the outcome. deliver() takes an optional idempotencyKey and returns what the provider reported, such as its message id:
const email = await c.get("email").build(args);
const { id } = await c.get("email").deliver(email, {
idempotencyKey: `order-receipt:${order.id}`, // same key on every retry
});Adapters whose provider deduplicates on a key, like Resend, forward it - so a retry after an unconfirmed send is not delivered twice. SMTP has no such feature and ignores it. Notification emails use this for every send.
Configuring Email Adapters
Set the adapter in the vitnode.api.config.ts of the app that serves your API (apps/web in a Single App, apps/api in a Monorepo App). Each provider ships as its own package. Install one, then pass it as email.adapter. The sender address belongs to the adapter.
1. Resend Adapter
import { ResendEmailAdapter } from '@vitnode/resend'
export const vitNodeApiConfig = buildApiConfig({
email: {
adapter: ResendEmailAdapter({
apiKey: process.env.RESEND_API_KEY,
from: process.env.RESEND_FROM_EMAIL,
}),
},
})See Resend for installation and setup.
2. SMTP Adapter (Nodemailer)
import { NodemailerEmailAdapter } from '@vitnode/nodemailer'
export const vitNodeApiConfig = buildApiConfig({
email: {
adapter: NodemailerEmailAdapter({
from: process.env.NODE_MAILER_FROM,
host: process.env.NODE_MAILER_HOST,
port: Number(process.env.NODE_MAILER_PORT ?? 587),
user: process.env.NODE_MAILER_USER,
password: process.env.NODE_MAILER_PASSWORD,
}),
},
})port defaults to 587 and secure to false. See Nodemailer for installation and setup.
Testing Email in AdminCP
Check your configuration in the AdminCP at System ā Integrations (/admin/core/system/integrations). Choose Test email on the Email card, enter an address and choose Send test email. The test goes through the same queue as every other email, so if it does not arrive, look for a failed send-email task in Debug Panel ā Queue, which shows the provider's error. The button needs the Send test email staff permission.