> ## 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 Express.js

> Add BosonMail email sending to an Express application using ESM, TypeScript, multer for attachments, and structured error handling.

This guide shows you how to integrate BosonMail into an Express application written in TypeScript and ESM. You will set up a shared client, create POST and GET endpoints for sending emails and checking status, handle file uploads with multer, and protect your routes so you do not create an open email relay.

> [!NOTE]
>
> This guide assumes Node.js 18 or later, Express 4 or 5, and TypeScript. The BosonMail SDK is ESM-only, so the examples use ESM syntax.

## Prerequisites

* Node.js 18 or later
* A BosonMail API key
* Express with TypeScript configured for ESM

## Project setup

**1. Install dependencies**

Install the SDK and optional multer for file uploads.

```bash theme={null}
npm install @bosonware-ent/bosonmail express
npm install --save-dev @types/express
npm install --save-dev multer @types/multer
```

**2. Configure environment variables**

Create a `.env` file and load it with `dotenv` in your entry point.

```bash .env theme={null}
BOSONMAIL_API_KEY=your_api_key
PORT=3000
```

## Shared client module

Create a single `BosonMailClient` instance and export it for reuse across routes.

```typescript lib/bosonmail.ts theme={null}
import BosonMailClient from "@bosonware-ent/bosonmail";

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

export const bosonmail = new BosonMailClient({ apiKey });
```

## Application entry point

Wire up JSON parsing, routes, and centralized error handling in your main file.

```typescript src/index.ts theme={null}
import "dotenv/config";
import express, { Request, Response, NextFunction } from "express";
import { bosonmail } from "./lib/bosonmail.js";

const app = express();
app.use(express.json());

app.get("/health", (_req, res) => {
  res.json({ ok: true });
});

// POST /api/send
app.post("/api/send", async (req, res, next) => {
  try {
    const to = typeof req.body.to === "string" ? req.body.to.trim() : "";
    const subject =
      typeof req.body.subject === "string" ? req.body.subject.trim() : "";
    const text =
      typeof req.body.text === "string" ? req.body.text.trim() : "";
    const html =
      typeof req.body.html === "string" ? req.body.html.trim() : undefined;

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

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

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

    res.status(result.statusCode ?? 200).json({
      success: true,
      id: result.id,
      messageId: result.messageId,
      status: result.status,
    });
  } catch (err) {
    next(err);
  }
});

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

    const result = await bosonmail.getEmailStatus(id);

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

    res.status(result.statusCode ?? 200).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,
    });
  } catch (err) {
    next(err);
  }
});

// Centralized error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error("Unhandled error:", err.message);
  res.status(500).json({ error: "Internal server error" });
});

const port = process.env.PORT ?? 3000;
app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`);
});
```

## Sending attachments with multer

Use `multer` with `memoryStorage` so you can pass file buffers directly to the SDK without writing to disk.

```typescript src/index.ts theme={null}
import "dotenv/config";
import express, { Request, Response, NextFunction } from "express";
import multer from "multer";
import { bosonmail } from "./lib/bosonmail.js";
import type { SendAttachment } from "@bosonware-ent/bosonmail";

const app = express();
app.use(express.json());

const upload = multer({
  storage: multer.memoryStorage(),
  limits: {
    fileSize: 10 * 1024 * 1024, // 10 MB per file
    files: 10,
  },
});

app.post(
  "/api/send-with-attachments",
  upload.array("files", 10),
  async (req, res, next) => {
    try {
      const to = typeof req.body.to === "string" ? req.body.to.trim() : "";
      const subject =
        typeof req.body.subject === "string" ? req.body.subject.trim() : "";
      const text =
        typeof req.body.text === "string" ? req.body.text.trim() : "";
      const html =
        typeof req.body.html === "string" ? req.body.html.trim() : undefined;

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

      const files = req.files as Express.Multer.File[] | undefined;
      const attachments: SendAttachment[] =
        files?.map((file) => ({
          filename: file.originalname,
          content: file.buffer,
          contentType: file.mimetype || "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;
        res.status(status).json({
          error: result.errorMessage,
          statusCode: result.statusCode,
        });
        return;
      }

      res.status(result.statusCode ?? 200).json({
        success: true,
        id: result.id,
        messageId: result.messageId,
        status: result.status,
      });
    } catch (err) {
      next(err);
    }
  }
);

// Centralized error handler
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
  console.error("Unhandled error:", err.message);
  res.status(500).json({ error: "Internal server error" });
});

const port = process.env.PORT ?? 3000;
app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}`);
});
```

> [!WARNING]
>
> Multer limits are set to match the SDK's per-attachment limit (10 MB) and maximum file count (10). The SDK also enforces a 20 MB total attachment limit. Validate uploads in your own middleware before they reach multer when possible.

## Protecting your endpoints

Never leave a send endpoint open to the public internet without authentication. At minimum:

* Require an API key or session cookie on `/api/send`.
* Add rate limiting per user or IP (for example, with `express-rate-limit`).
* For public contact forms, use a CAPTCHA and strict recipient allowlists so you cannot be used as an open relay.

```typescript theme={null}
import rateLimit from "express-rate-limit";

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,
  message: { error: "Too many requests" },
});

app.use("/api/send", limiter);
```

## Error response mapping

Return consistent HTTP status codes so callers can distinguish client errors from upstream failures.

| 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.