Back to blog
HonoTutorialIntegration
How to Send Transactional Emails from Hono
Send emails from Hono on Node.js, Cloudflare Workers, Bun, or Deno. Covers setup, validation, error handling, and async patterns.
Johan SteniusDecember 29, 20253 min read
Hono is what we use for SendPigeon's API. We picked it because it's built on standard Web APIs instead of Node-specific stuff. Your application logic stays portable across Node.js, Cloudflare Workers, Bun, and Deno — only the server setup differs.
Here's how to send transactional emails from Hono.
TL;DR
Quick setup:
npm install sendpigeon- Add
SENDPIGEON_API_KEYto your environment - Access via
process.env,c.env,Bun.env, orDeno.env
Basic setup
npm install sendpigeon hono
import { Hono } from "hono";
import { SendPigeon } from "sendpigeon";
const app = new Hono();
app.post("/send", async (c) => {
const client = new SendPigeon(process.env.SENDPIGEON_API_KEY!);
const { to, subject, html } = await c.req.json();
const { data, error } = await client.send({
from: "hello@yourdomain.com",
to,
subject,
html,
});
if (error) {
return c.json({ error: error.message }, error.status);
}
return c.json({ id: data.id });
});
export default app;
Environment by runtime
Node.js
import { serve } from "@hono/node-server";
const client = new SendPigeon(process.env.SENDPIGEON_API_KEY!);
serve({ fetch: app.fetch, port: 3000 });
Cloudflare Workers
type Bindings = { SENDPIGEON_API_KEY: string };
const app = new Hono<{ Bindings: Bindings }>();
app.post("/send", async (c) => {
const client = new SendPigeon(c.env.SENDPIGEON_API_KEY);
// ...
});
Bun
const client = new SendPigeon(Bun.env.SENDPIGEON_API_KEY!);
Deno
const client = new SendPigeon(Deno.env.get("SENDPIGEON_API_KEY")!);
Validation with Zod
Use @hono/zod-openapi for validated, typed requests:
import { OpenAPIHono, createRoute, z } from "@hono/zod-openapi";
const sendEmailSchema = z.object({
to: z.string().email(),
subject: z.string().min(1).max(200),
html: z.string(),
});
const sendRoute = createRoute({
method: "post",
path: "/send",
request: {
body: { content: { "application/json": { schema: sendEmailSchema } } },
},
responses: {
200: {
description: "Email sent",
content: { "application/json": { schema: z.object({ id: z.string() }) } },
},
},
});
const app = new OpenAPIHono();
app.openapi(sendRoute, async (c) => {
const body = c.req.valid("json"); // Fully typed
// ...
});
Error handling
const { data, error } = await client.send({ from, to, subject, html });
if (error) {
if (error.status === 429) {
return c.json({ error: "Rate limited" }, 429);
}
return c.json({ error: error.message }, error.status || 500);
}
return c.json({ id: data.id });
Non-blocking emails
Cloudflare Workers:
app.post("/signup", async (c) => {
const user = await createUser(data);
c.executionCtx.waitUntil(sendWelcomeEmail(user.email));
return c.json({ userId: user.id });
});
Node.js:
app.post("/signup", async (c) => {
const user = await createUser(data);
sendWelcomeEmail(user.email).catch(console.error);
return c.json({ userId: user.id });
});
Testing
import { describe, it, expect } from "vitest";
import app from "./app";
describe("POST /send", () => {
it("sends email", async () => {
const res = await app.request("/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
to: "user@example.com",
subject: "Test",
html: "<p>Hello</p>",
}),
});
expect(res.status).toBe(200);
});
});
Next steps
- Framework Guides — More integrations
- Email Authentication — SPF, DKIM, DMARC
- API Docs — Full reference