Send email with Hono
Send transactional email from Hono through PostStack.
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.
Quickstart#
Install the SDK
bashbun add @poststack.dev/sdk honoInitialize the client
typescriptimport { PostStack } from '@poststack.dev/sdk'; import { Hono } from 'hono'; const poststack = new PostStack(process.env.POSTSTACK_API_KEY!); const app = new Hono();Send an email
typescriptimport { 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;Handle errors
typescriptimport { 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
The SDK runs on Workers unmodified — it only needs fetch. Workers have no process.env by default: store the key with wrangler secret put POSTSTACK_API_KEY, type the app as new Hono<{ Bindings: { POSTSTACK_API_KEY: string } }>(), and create the client inside the handler from c.env.POSTSTACK_API_KEY.
Hono on Vercel Edge
In a Next.js project on Vercel, mount the app in app/api/[[...route]]/route.ts with new Hono().basePath("/api") and export const POST = handle(app) (handle from hono/vercel). Add export const runtime = "edge" to run it on the Edge Runtime; the SDK works on both runtimes.
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. Read the key from c.env (a Workers secret) rather than process.env.
What about Vercel Edge?
Yes — mount the app with handle from hono/vercel and set export const runtime = "edge"; the SDK runs unchanged.
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.
Ready to send from Hono?
Create a free account and an API key — no card needed.