Skip to content

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#

Before you start

You need an API key (sk_live_…, or sk_test_… to try it without delivering anything) and a sending domain you have verified in PostStack. See Authentication and Domains.

  1. Install the SDK

    bash
    bun add @poststack.dev/sdk hono
  2. Initialize the client

    typescript
    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

    typescript
    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

    typescript
    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

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.

Get started free

Next steps

Was this page helpful?