Send emails with Hono
Learn how to send transactional emails using PostStack and Hono.
Hono is the framework PostStack itself is built on, so the TypeScript SDK is exercised heavily against the runtime. The combination is fast (sub-millisecond router lookup, tree-shaken bundle), portable (runs on Bun, Node, Cloudflare Workers, Deno, AWS Lambda, Vercel Edge) and ergonomic. Reach for Hono when you want a small, type-safe alternative to Express that scales to the Edge.
1. Install the SDK
bun add @poststack.dev/sdk hono2. Initialize the client
import { PostStack } from '@poststack.dev/sdk';
import { Hono } from 'hono';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const app = new Hono();3. Send an email
import { PostStack, PostStackError } from '@poststack.dev/sdk';
import { Hono } from 'hono';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const app = new Hono();
app.post('/send', async (c) => {
const { to, subject, html } = await c.req.json<{
to: string | string[];
subject: string;
html: string;
}>();
try {
const { id } = await poststack.emails.send({
from: 'hello@yourdomain.com',
to: Array.isArray(to) ? to : [to],
subject,
html,
});
return c.json({ id });
} catch (err) {
if (err instanceof PostStackError) {
return c.json({ error: err.message }, err.statusCode as ContentfulStatusCode);
}
throw err;
}
});
export default app;4. Handle errors
Hono idioms for error handling, retries, and structured logging when calling the PostStack API.
import { PostStack, PostStackError } from '@poststack.dev/sdk';
import { Hono } from 'hono';
import { logger } from 'hono/logger';
import type { ContentfulStatusCode } from 'hono/utils/http-status';
const poststack = new PostStack(process.env.POSTSTACK_API_KEY!);
const app = new Hono();
app.use('*', logger());
app.post('/send', async (c) => {
const body = await c.req.json<{ to: string; subject: string; html: string }>();
try {
const { id } = await poststack.emails.send({
from: 'hello@yourdomain.com',
to: [body.to],
subject: body.subject,
html: body.html,
});
return c.json({ id });
} catch (err) {
if (err instanceof PostStackError) {
return c.json(
{ error: err.message, requestId: err.requestId },
err.statusCode as ContentfulStatusCode,
);
}
throw err;
}
});
// Catch-all error handler — runs for thrown errors that route handlers
// did not handle themselves. Always include for production deploys.
app.onError((err, c) => {
console.error('Unhandled error', err);
return c.json({ error: 'Internal Server Error' }, 500);
});
export default app;Framework integrations
Hono on Cloudflare Workers
Deploy the same code to Workers via `wrangler deploy`. The PostStack SDK runs on Workers without modification — fetch is native and the bundle stays under the 1 MB free-tier limit. Set the API key via `wrangler secret put`.
Hono on Vercel Edge
Hono auto-detects the Vercel runtime. Export the app from `app/api/[...route]/route.ts` with `export const runtime = "edge"` and the SDK runs on every Vercel edge node. Globally distributed sends without managing infrastructure.
Hono on Bun.serve
For self-hosted deploys, `Bun.serve({ fetch: app.fetch })` is enough. The same code that runs on Workers/Edge runs on a single Bun server. PostStack uses this pattern internally.
Zod-validated handlers
Pair `@hono/zod-validator` with `z.object({...})` to validate request bodies before reaching the SDK call. Invalid payloads return a structured 400 before any API call is made, saving rate-limit budget.
Common pitfalls
Casting statusCode without `ContentfulStatusCode`
Hono’s `c.json(body, status)` requires a status code typed as `ContentfulStatusCode`. PostStackError’s `statusCode` is `number`. Cast with `as ContentfulStatusCode` (or narrow at the call site) so TypeScript stays happy.
Mixing `c.req.json()` and validators
If you use `@hono/zod-validator`, do not also call `c.req.json()` — that bypasses validation. Always read the validated body through `c.req.valid("json")`.
Sharing the client across Workers isolates
On Cloudflare Workers, each isolate gets its own module scope. The SDK client is instantiated once per isolate, which is correct. Trying to memoise across isolates is unnecessary and unsupported.
FAQ
Does the SDK run on Cloudflare Workers?
Yes — Hono and the PostStack SDK both run on Workers without any adapter. Bundle stays small and start time is fast.
What about Vercel Edge?
Yes — set `export const runtime = "edge"` and the SDK runs unchanged. Globally distributed sends, no Lambda cold start.
Why use Hono over Express?
Smaller bundle, faster router, portable across Node/Bun/Workers/Edge, end-to-end typed routes for RPC, and built-in validators. Express remains a fine choice for traditional Node deploys.
Related guides
Ready to send emails with Hono?
Create a free account and get your API key in under a minute.