eusend
Frameworks

Next.js

Send transactional email from a Next.js app through eusend — from a Server Action or a Route Handler, with React Email templates and without blocking the response.

Next.js runs your sending code on the server, so the eusend SDK works as it does anywhere else in Node.js. This page covers the two places a send usually lives — a Server Action and a Route Handler — and the two things that go wrong most often: a key that leaks into the client bundle, and a serverless function frozen before the request to eusend finishes.

Setup

Install the SDK

npm install @eusend_dev/sdk

Add the API key

Create a key under API keys. Keep it out of any variable prefixed NEXT_PUBLIC_ — those are inlined into the browser bundle.

.env.local
EUSEND_API_KEY=eu_test_...

An eu_test_ key runs validation, quota and suppression checks and shows the email in your log without delivering it. Switch to an eu_live_ key when you deploy.

Create one client

lib/eusend.ts
import 'server-only'
import { Eusend } from '@eusend_dev/sdk'

export const eusend = new Eusend(process.env.EUSEND_API_KEY)

server-only turns an accidental import from a Client Component into a build error.

Send from a Server Action

app/contact/actions.ts
'use server'

import { eusend } from '@/lib/eusend'

export async function sendContactMessage(formData: FormData) {
  const email = String(formData.get('email') ?? '')
  const message = String(formData.get('message') ?? '')

  const { error } = await eusend.emails.send({
    from: 'Acme <hello@yourdomain.com>',
    to: 'support@yourdomain.com',
    replyTo: email,
    subject: 'New contact form message',
    text: message,
  })

  if (error) return { ok: false, code: error.name }
  return { ok: true }
}

The SDK returns { data, error } instead of throwing, so a failed send is a value you can return to the form. Branch on error.name — the error code — not on the message.

⚠

A public form that sends email is an open relay unless you bound it. Send to your own address and put the visitor in replyTo, as above, and rate-limit the action. Never let a form field choose the recipient.

Send from a Route Handler

app/api/receipt/route.ts
import { eusend } from '@/lib/eusend'

export async function POST(request: Request) {
  const { orderId, email } = await request.json()

  const { data, error } = await eusend.emails.send(
    {
      from: 'Acme <billing@yourdomain.com>',
      to: email,
      subject: `Receipt for order ${orderId}`,
      html: `<p>Thanks — order ${orderId} is confirmed.</p>`,
    },
    { idempotencyKey: `receipt-${orderId}` },
  )

  if (error) return Response.json({ code: error.name }, { status: error.statusCode ?? 502 })
  return Response.json({ id: data.id })
}

The idempotencyKey makes a retried request — a double click, a webhook redelivery — return the original email instead of sending a second one.

React Email templates

Pass a component as react and the SDK renders it to HTML before sending:

app/api/welcome/route.tsx
import { eusend } from '@/lib/eusend'
import { WelcomeEmail } from '@/emails/welcome'

export async function POST(request: Request) {
  const { name, email } = await request.json()

  const { error } = await eusend.emails.send({
    from: 'Acme <hello@yourdomain.com>',
    to: email,
    subject: 'Welcome to Acme',
    react: <WelcomeEmail name={name} />,
  })

  return Response.json({ ok: !error })
}

This needs react and @react-email/render installed. The file must be .tsx.

Don't block the response

Sending adds a network round trip to the request. When the user doesn't need the result, schedule it with after so the response goes out first and the platform keeps the function alive until the send finishes:

import { after } from 'next/server'

after(async () => {
  const { error } = await eusend.emails.send({ ... })
  if (error) console.error('welcome email failed', error.name)
})

Without after, a send you don't await can be cut off when a serverless function is frozen after responding.

Next steps