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

> Add BosonMail email sending to a Next.js App Router project using route handlers, server actions, and environment-safe configuration.

This guide shows you how to integrate BosonMail into a Next.js App Router project. You will create a server-only client module, wire up a route handler for form submissions, add a server action for direct component calls, and expose a status endpoint so your UI can track delivery.

> [!NOTE]
>
> This guide assumes you are using Next.js 14 or later with the App Router. All BosonMail SDK calls must run on the server. Never expose your API key to the browser.

## Prerequisites

* A Next.js project with TypeScript
* A BosonMail API key
* Node.js 18 or later

## Project setup

**1. Install the SDK**

Install the BosonMail SDK in your Next.js project.

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

**2. Add environment variables**

Create or update `.env.local` in your project root. Do **not** prefix the key with `NEXT_PUBLIC_`, because that would expose it to client bundles.

```bash .env.local theme={null}
BOSONMAIL_API_KEY=your_api_key
```

## Create a server-only client

Create a shared module that instantiates `BosonMailClient` once and exports it for use across server code.

```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 });
```

> [!TIP]
>
> You can optionally install the `server-only` package and add `import "server-only"` at the top of `lib/bosonmail.ts`. This causes Next.js to throw a build-time error if the module is ever imported into client code.

## Route handler for sending email

Create a route handler that accepts a POST request, validates the body, sends the email, and returns a structured JSON response.

```typescript app/api/contact/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { bosonmail } from "@/lib/bosonmail";

export const runtime = "nodejs";

export async function POST(request: NextRequest) {
  let body: Record<string, unknown>;
  try {
    body = await request.json();
  } catch {
    return NextResponse.json({ error: "Invalid JSON body" }, { status: 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 NextResponse.json({ error: "'to' is required" }, { status: 400 });
  }
  if (!subject) {
    return NextResponse.json({ error: "'subject' is required" }, { status: 400 });
  }
  if (!text && !html) {
    return NextResponse.json(
      { error: "Email must contain text or HTML content" },
      { status: 400 }
    );
  }

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

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

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

> [!NOTE]
>
> The `runtime = "nodejs"` export is explicit. The BosonMail SDK relies on web-standard globals available in Node.js 18+, but BosonWare has not verified edge-runtime compatibility, so pinning to the Node.js runtime avoids surprises.

### Optional: validate with Zod

If you prefer schema validation, install Zod and replace the manual checks.

```bash theme={null}
npm install zod
```

```typescript app/api/contact/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { bosonmail } from "@/lib/bosonmail";

export const runtime = "nodejs";

const schema = z.object({
  to: z.string().min(1, "'to' is required"),
  subject: z.string().min(1, "'subject' is required"),
  text: z.string().optional(),
  html: z.string().optional(),
});

export async function POST(request: NextRequest) {
  let body: unknown;
  try {
    body = await request.json();
  } catch {
    return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
  }

  const parsed = schema.safeParse(body);
  if (!parsed.success) {
    return NextResponse.json(
      { error: parsed.error.errors.map((e) => e.message).join("; ") },
      { status: 400 }
    );
  }

  const { to, subject, text, html } = parsed.data;
  if (!text && !html) {
    return NextResponse.json(
      { error: "Email must contain text or HTML content" },
      { status: 400 }
    );
  }

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

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

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

## Server action for sending email

If you prefer server actions over route handlers, create a callable action that validates inputs and returns a typed result.

```typescript app/actions/send-email.ts theme={null}
"use server";

import { bosonmail } from "@/lib/bosonmail";

export interface SendEmailResult {
  success: boolean;
  id?: string;
  messageId?: string;
  status?: string;
  error?: string;
  statusCode?: number;
}

export async function sendContactEmail(
  to: string,
  subject: string,
  text: string,
  html?: string
): Promise<SendEmailResult> {
  if (!to.trim()) {
    return { success: false, error: "'to' is required" };
  }
  if (!subject.trim()) {
    return { success: false, error: "'subject' is required" };
  }
  if (!text.trim() && !html?.trim()) {
    return { success: false, error: "Email must contain text or HTML content" };
  }

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

  if (!result.success) {
    return {
      success: false,
      error: result.errorMessage,
      statusCode: result.statusCode,
    };
  }

  return {
    success: true,
    id: result.id,
    messageId: result.messageId,
    status: result.status,
  };
}
```

## Route handler for email status

Expose a status endpoint so your frontend can poll or fetch delivery state. In recent Next.js versions, `params` is a `Promise`, so you must `await` it inside the handler.

```typescript app/api/emails/[id]/status/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { bosonmail } from "@/lib/bosonmail";

export const runtime = "nodejs";

export async function GET(
  _request: NextRequest,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params;
  const trimmed = id?.trim();
  if (!trimmed) {
    return NextResponse.json(
      { error: "'messageId' is required" },
      { status: 400 }
    );
  }

  const result = await bosonmail.getEmailStatus(trimmed);

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

  return NextResponse.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,
    },
    { status: result.statusCode ?? 200 }
  );
}
```

## Sending attachments via route handler

If your form accepts file uploads, use `request.formData()` and pass each file to the SDK as a `Blob`.

```typescript app/api/contact-with-attachment/route.ts theme={null}
import { NextRequest, NextResponse } from "next/server";
import { bosonmail } from "@/lib/bosonmail";
import type { SendAttachment } from "@bosonware-ent/bosonmail";

export const runtime = "nodejs";

export async function POST(request: NextRequest) {
  const form = await request.formData();

  const to = (form.get("to") as string)?.trim() ?? "";
  const subject = (form.get("subject") as string)?.trim() ?? "";
  const text = (form.get("text") as string)?.trim() ?? "";
  const html = (form.get("html") as string)?.trim() || undefined;

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

  const attachments: SendAttachment[] = [];
  const files = form.getAll("files");
  for (const file of files) {
    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 NextResponse.json(
      { error: result.errorMessage, statusCode: result.statusCode },
      { status }
    );
  }

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

> [!WARNING]
>
> Do not accept unauthenticated file uploads in production. Add your own authentication, rate limiting, and file-type validation before passing files to BosonMail.

## Deployment notes

* Set `BOSONMAIL_API_KEY` in your hosting environment (Vercel project settings, or your deployment platform's secret manager).
* Do not commit `.env.local` to version control.
* If you use the Node.js runtime explicitly, confirm your platform supports it. Vercel defaults to Node.js for App Router API routes, but the explicit export prevents accidental edge-runtime selection.

## Error response mapping

Map SDK results to HTTP status codes so your frontend can react consistently.

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