Next.js

A server action and a route handler, with the key kept out of the browser bundle.

The only rule that matters in Next.js is that the send happens on the server. A key in a client component is a key in the JavaScript bundle, and anybody can read it. Server actions, route handlers, and server components are all fine; "use client" files are not.

Install

App Router, Next 14 or 15
npm install @emails.sh/sdk
.env.local
# .env.local, which create-next-app already gitignores.
# The key comes from https://emails.sh/dashboard/api-keys.
EMAILSSH_API_KEY=esh_your_key_here

One client for the app

lib/emails.ts
// lib/emails.ts
import 'server-only';
import { Emailssh } from '@emails.sh/sdk';

if (!process.env.EMAILSSH_API_KEY) {
  throw new Error('EMAILSSH_API_KEY is not set. Add it to .env.local.');
}

export const mail = new Emailssh({ apiKey: process.env.EMAILSSH_API_KEY });

The server-only import turns an accidental import from a client component into a build error rather than a leaked key at runtime. Install it with npm install server-only.

A server action

app/actions/subscribe.ts
// app/actions/subscribe.ts
'use server';

import { mail } from '@/lib/emails';

export async function subscribe(formData: FormData) {
  const address = String(formData.get('email') ?? '');
  if (!address.includes('@')) return { error: 'That does not look like an email address.' };

  const { id } = await mail.send({
    from: 'Acme <hello@acme.com>',
    to: [address],
    subject: 'Confirm your subscription',
    html: '<p>Click the link in this email to confirm.</p>',
    text: 'Click the link in this email to confirm.',
    idempotencyKey: `subscribe-${address}`
  });

  return { id };
}

Or a route handler

app/api/send/route.ts
// app/api/send/route.ts
import { NextResponse } from 'next/server';
import { mail } from '@/lib/emails';

export async function POST(request: Request) {
  const { to, subject, html } = await request.json();

  try {
    const { id } = await mail.send({ from: 'Acme <hello@acme.com>', to: [to], subject, html });
    return NextResponse.json({ id });
  } catch (err) {
    // The message says what to do about it. Log it; do not show it to the user.
    console.error('emails.sh refused the send', err);
    return NextResponse.json({ error: 'Could not send that email.' }, { status: 502 });
  }
}

On Vercel, set EMAILSSH_API_KEY in the project's environment variables for every environment you deploy, then redeploy. A variable added without a redeploy is not in the running build.