> ## Documentation Index
> Fetch the complete documentation index at: https://docs.bosonware.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Integrating with Hono

> Add BosonMail email sending to Hono on Node.js and Cloudflare Workers, with environment bindings, validation, and attachment handling.

This guide shows you how to integrate BosonMail into Hono applications running on Node.js and on Cloudflare Workers. You will wire environment variables, validate requests, send emails, check status, and handle file uploads using Hono's built-in helpers.

> [!NOTE]
>
> The BosonMail SDK uses only web-standard globals: `fetch`, `FormData`, `Blob`, and `AbortController`. It has no Node-specific imports, so it is expected to work in any runtime that provides these globals. BosonWare has not officially verified Cloudflare Workers support, so test before production.

## Prerequisites

* A Hono project on Node.js or Cloudflare Workers
* A BosonMail API key
* Node.js 18 or later (for the Node.js section)

## Hono on Node.js

### Install dependencies

```bash theme={null}
npm install hono @hono/node-server @bosonware-ent/bosonmail
```

### Create the application

Use `process.env` for the API key and mount routes for sending and status checks.

```typescript src/index.ts theme={null}
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { HTTPException } from "hono/http-exception";
import BosonMailClient from "@bosonware-ent/bosonmail";
import type { SendAttachment } from "@bosonware-ent/bosonmail";

const apiKey = process.env.BOSONMAIL_API_KEY;
if (!apiKey) {
  throw new Error("BOSONMAIL_API_KEY is not set");
}

const bosonmail = new BosonMailClient({ apiKey });
const app = new Hono();

app.post("/api/send", async (c) => {
  let body: Record<string, unknown>;
  try {
    body = await c.req.json();
  } catch {
    throw new HTTPException(400, { message: "Invalid JSON body" });
  }

  const to = typeof body.to === "string" ? body.to.trim() : "";
  const subject = typeof body.subject === "string" ? body.subject.trim() : "";
  const text = typeof body.text === "string" ? body.text.trim() : "";
  const html = typeof body.html === "string" ? body.html.trim() : undefined;

  if (!to) {
    throw new HTTPException(400, { message: "'to' is required" });
  }
  if (!subject) {
    throw new HTTPException(400, { message: "'subject' is required" });
  }
  if (!text && !html) {
    throw new HTTPException(400, {
      message: "Email must contain text or HTML content",
    });
  }

  const result = await bosonmail.sendEmail(to, subject, { text, html });

  if (!result.success) {
    const status = result.statusCode ?? 502;
    return c.json(
      { error: result.errorMessage, statusCode: result.statusCode },
      status as any
    );
  }

  return c.json(
    {
      success: true,
      id: result.id,
      messageId: result.messageId,
      status: result.status,
    },
    result.statusCode ?? 200
  );
});

app.get("/api/emails/:id/status", async (c) => {
  const id = c.req.param("id")?.trim();
  if (!id) {
    throw new HTTPException(400, { message: "'messageId' is required" });
  }

  const result = await bosonmail.getEmailStatus(id);

  if (!result.success) {
    const status = result.statusCode ?? 502;
    return c.json(
      { error: result.errorMessage, errorCode: result.errorCode },
      status as any
    );
  }

  return c.json(
    {
      success: true,
      id: result.data?.id,
      uuid: result.data?.uuid,
      messageId: result.data?.message_id,
      status: result.data?.status,
      deliveryStatus: result.data?.delivery_status,
      recipient: result.data?.recipient,
      subject: result.data?.subject,
      createdAt: result.data?.created_at,
    },
    result.statusCode ?? 200
  );
});

app.post("/api/send-with-attachments", async (c) => {
  const body = await c.req.parseBody();

  const to = typeof body.to === "string" ? body.to.trim() : "";
  const subject =
    typeof body.subject === "string" ? body.subject.trim() : "";
  const text = typeof body.text === "string" ? body.text.trim() : "";
  const html = typeof body.html === "string" ? body.html.trim() : undefined;

  if (!to) {
    throw new HTTPException(400, { message: "'to' is required" });
  }
  if (!subject) {
    throw new HTTPException(400, { message: "'subject' is required" });
  }
  if (!text && !html) {
    throw new HTTPException(400, {
      message: "Email must contain text or HTML content",
    });
  }

  const attachments: SendAttachment[] = [];
  const files = body.files;
  const fileList = Array.isArray(files) ? files : files ? [files] : [];
  for (const file of fileList) {
    if (!(file instanceof File)) continue;
    attachments.push({
      filename: file.name,
      content: file,
      contentType: file.type || "application/octet-stream",
    });
  }

  const result = await bosonmail.sendEmail(to, subject, {
    text,
    html,
    attachments: attachments.length > 0 ? attachments : undefined,
  });

  if (!result.success) {
    const status = result.statusCode ?? 502;
    return c.json(
      { error: result.errorMessage, statusCode: result.statusCode },
      status as any
    );
  }

  return c.json(
    {
      success: true,
      id: result.id,
      messageId: result.messageId,
      status: result.status,
    },
    result.statusCode ?? 200
  );
});

serve({
  fetch: app.fetch,
  port: Number(process.env.PORT ?? 3000),
});
```

> [!NOTE]
>
> `c.req.parseBody()` returns `File` objects when the request is `multipart/form-data`. You can pass these directly to the SDK because `File` extends `Blob`.

## Hono on Cloudflare Workers

### Environment bindings

On Workers, store the API key as a secret binding and access it via `c.env`.

```typescript src/index.ts theme={null}
import { Hono } from "hono";
import BosonMailClient from "@bosonware-ent/bosonmail";
import type { SendAttachment } from "@bosonware-ent/bosonmail";

type Bindings = {
  BOSONMAIL_API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

function getClient(c: { env: Bindings }) {
  return new BosonMailClient({ apiKey: c.env.BOSONMAIL_API_KEY });
}

app.post("/api/send", async (c) => {
  let body: Record<string, unknown>;
  try {
    body = await c.req.json();
  } catch {
    return c.json({ error: "Invalid JSON body" }, 400);
  }

  const to = typeof body.to === "string" ? body.to.trim() : "";
  const subject = typeof body.subject === "string" ? body.subject.trim() : "";
  const text = typeof body.text === "string" ? body.text.trim() : "";
  const html = typeof body.html === "string" ? body.html.trim() : undefined;

  if (!to) {
    return c.json({ error: "'to' is required" }, 400);
  }
  if (!subject) {
    return c.json({ error: "'subject' is required" }, 400);
  }
  if (!text && !html) {
    return c.json(
      { error: "Email must contain text or HTML content" },
      400
    );
  }

  const client = getClient(c);
  const result = await client.sendEmail(to, subject, { text, html });

  if (!result.success) {
    const status = result.statusCode ?? 502;
    return c.json(
      { error: result.errorMessage, statusCode: result.statusCode },
      status as any
    );
  }

  return c.json(
    {
      success: true,
      id: result.id,
      messageId: result.messageId,
      status: result.status,
    },
    result.statusCode ?? 200
  );
});

app.get("/api/emails/:id/status", async (c) => {
  const id = c.req.param("id")?.trim();
  if (!id) {
    return c.json({ error: "'messageId' is required" }, 400);
  }

  const client = getClient(c);
  const result = await client.getEmailStatus(id);

  if (!result.success) {
    const status = result.statusCode ?? 502;
    return c.json(
      { error: result.errorMessage, errorCode: result.errorCode },
      status as any
    );
  }

  return c.json(
    {
      success: true,
      id: result.data?.id,
      uuid: result.data?.uuid,
      messageId: result.data?.message_id,
      status: result.data?.status,
      deliveryStatus: result.data?.delivery_status,
      recipient: result.data?.recipient,
      subject: result.data?.subject,
      createdAt: result.data?.created_at,
    },
    result.statusCode ?? 200
  );
});

export default app;
```

### Secrets and local development

Upload your secret to Workers:

```bash theme={null}
wrangler secret put BOSONMAIL_API_KEY
```

For local development, create `.dev.vars`:

```bash .dev.vars theme={null}
BOSONMAIL_API_KEY=your_api_key
```

> [!WARNING]
>
> Cloudflare Workers CPU and wall-clock limits apply. Do not implement status polling loops inside a request handler. If you need periodic reconciliation, use a Cron Trigger or a queue consumer instead.

## Optional: validation with Zod

You can add `@hono/zod-validator` for schema-based validation if you want stricter type checking.

```bash theme={null}
npm install zod @hono/zod-validator
```

```typescript theme={null}
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";

const sendSchema = z.object({
  to: z.string().min(1),
  subject: z.string().min(1),
  text: z.string().optional(),
  html: z.string().optional(),
});

app.post("/api/send", zValidator("json", sendSchema), async (c) => {
  const { to, subject, text, html } = c.req.valid("json");
  if (!text && !html) {
    return c.json(
      { error: "Email must contain text or HTML content" },
      400
    );
  }
  const result = await getClient(c).sendEmail(to, subject, { text, html });
  // ...
});
```

## Error response mapping

Return consistent HTTP status codes across both runtimes.

| SDK outcome | Suggested HTTP status |
| - | - |
| Client validation failed | 400 |
| `result.statusCode` from API (e.g. 401, 403, 404) | Forward as-is |
| Upstream failure with no `statusCode` | 502 |
| Timeout with no `statusCode` | 504 |

## Related

> [card] [Sending emails](https://docs.bosonware.com/sdk/sending-emails.md)
>
> SDK reference for sendEmail and its options.

> [card] [Email status](https://docs.bosonware.com/sdk/email-status.md)
>
> How to check delivery status with getEmailStatus.

> [card] [Production best practices](https://docs.bosonware.com/guides/production-best-practices.md)
>
> Key management, retries, logging, and monitoring guidance.

> [card] [Attachments](https://docs.bosonware.com/sdk/attachments.md)
>
> SDK reference for sending file attachments.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.