/ astro-integrations / How to Integrate Resend with Astro: Complete Guide
astro-integrations 10 min read

How to Integrate Resend with Astro: Complete Guide

Step-by-step guide to integrating Resend with your Astro website. Setup, configuration, and best practices.

How to Integrate Resend with Astro: Complete Guide

How to Integrate Resend with Astro: Complete Guide

Resend is a modern email API built for developers, created by the same team behind React Email. It provides a clean API for sending transactional emails, supports custom domains, and works well with modern frameworks. Combined with Astro's server-side capabilities, Resend lets you send emails from contact forms, sign-up flows, and notification systems directly from your Astro project.

This guide covers setting up Resend in an Astro project, sending plain text and HTML emails, using React Email templates, and building a working contact form.

Prerequisites

You will need:

  • Node.js v22.12.0 or higher (the minimum Astro 6 requires; odd-numbered releases like v23 are not supported)
  • An existing Astro project. Astro is on v6 now, and on-demand rendering works the same way from v5 onward
  • A Resend account (free tier at resend.com, 100 emails/day, 3,000 emails/month)
  • A Resend API key (found in your Resend dashboard under API Keys)
  • A verified domain (optional but recommended for production)

The versions referenced throughout this guide were the current published releases on npm as of 2026-05-29:

  • resend 6.12.4
  • @astrojs/node 10.1.2
  • @react-email/components 1.0.12
  • astro 6.4.2
  • react and react-dom 19.2.6

Always pin against npm view <package> version at the time you install, since these move quickly.

Installation

Install the Resend SDK:

npm install resend

If you want to use React Email templates for styled emails:

npm install @react-email/components react react-dom

Sending email requires a route that runs on the server, and Astro renders everything statically by default. Add an SSR adapter so individual routes can opt into on-demand rendering. The official Astro astro add command installs the adapter and wires it into your config in one step:

npx astro add node

This installs @astrojs/node and adds the adapter block to astro.config.mjs for you.

Configuration

Environment Variables

Add your Resend API key to .env:

RESEND_API_KEY=re_your_api_key_here

Astro exposes variables without a PUBLIC_ prefix to server-side code only, so a key referenced as import.meta.env.RESEND_API_KEY from inside an API route or action never reaches the browser bundle.

Creating the Resend Client

Set up a reusable Resend instance:

// src/lib/resend.ts
import { Resend } from 'resend';

export const resend = new Resend(import.meta.env.RESEND_API_KEY);

Astro Config

This is the single most common thing people get wrong on older guides. The output: 'hybrid' value was removed in Astro 5 and does not exist in Astro 6. Astro now builds to static by default, and you opt individual routes into on-demand rendering. So the config only needs the adapter, no output key:

// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';

export default defineConfig({
  adapter: node({ mode: 'standalone' }),
});

With this setup the site stays static by default, and any route that needs the server (your email API endpoint, contact form, or action) declares export const prerender = false. If you would rather render everything on the server, set output: 'server' instead, then mark the static pages with export const prerender = true. For an email-sending blog, the static default plus per-route opt-out is almost always what you want.

Common Patterns

Sending a Simple Email

Create an API endpoint that sends a plain text email. Because the project is static by default, the endpoint must opt into on-demand rendering with export const prerender = false:

// src/pages/api/send-email.ts
import type { APIRoute } from 'astro';
import { resend } from '../../lib/resend';

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const body = await request.json();

  try {
    const { data, error } = await resend.emails.send({
      from: 'Your Site <noreply@yourdomain.com>',
      to: body.to,
      subject: body.subject,
      text: body.message,
    });

    if (error) {
      return new Response(JSON.stringify({ error }), { status: 400 });
    }

    return new Response(JSON.stringify({ id: data?.id }), { status: 200 });
  } catch (err) {
    return new Response(JSON.stringify({ error: 'Failed to send email' }), { status: 500 });
  }
};

Building a Contact Form

Create a contact page with a form that sends emails through your API:

---
// src/pages/contact.astro
export const prerender = false;

let success = false;
let errorMessage = '';

if (Astro.request.method === 'POST') {
  try {
    const formData = await Astro.request.formData();
    const name = formData.get('name') as string;
    const email = formData.get('email') as string;
    const message = formData.get('message') as string;

    const { resend } = await import('../lib/resend');

    const { error } = await resend.emails.send({
      from: 'Contact Form <noreply@yourdomain.com>',
      to: 'you@yourdomain.com',
      replyTo: email,
      subject: `Contact from ${name}`,
      html: `
        <h2>New Contact Form Submission</h2>
        <p><strong>Name:</strong> ${name}</p>
        <p><strong>Email:</strong> ${email}</p>
        <p><strong>Message:</strong></p>
        <p>${message}</p>
      `,
    });

    if (error) {
      errorMessage = 'Failed to send message. Please try again.';
    } else {
      success = true;
    }
  } catch {
    errorMessage = 'Something went wrong.';
  }
}
---

<html>
  <body>
    <h1>Contact Us</h1>

    {success && <p style="color: green;">Message sent successfully!</p>}
    {errorMessage && <p style="color: red;">{errorMessage}</p>}

    <form method="POST">
      <label>Name <input type="text" name="name" required /></label>
      <label>Email <input type="email" name="email" required /></label>
      <label>Message <textarea name="message" rows="5" required></textarea></label>
      <button type="submit">Send Message</button>
    </form>
  </body>
</html>

Using React Email Templates

For more polished emails, create React Email components:

// src/emails/WelcomeEmail.tsx
import { Html, Head, Body, Container, Text, Button, Heading } from '@react-email/components';

interface WelcomeEmailProps {
  name: string;
  loginUrl: string;
}

export default function WelcomeEmail({ name, loginUrl }: WelcomeEmailProps) {
  return (
    <Html>
      <Head />
      <Body style={{ backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' }}>
        <Container style={{ padding: '40px 20px', maxWidth: '560px', margin: '0 auto' }}>
          <Heading>Welcome, {name}!</Heading>
          <Text>Thanks for signing up. Click below to get started.</Text>
          <Button
            href={loginUrl}
            style={{
              backgroundColor: '#5046e5',
              color: '#fff',
              padding: '12px 24px',
              borderRadius: '6px',
              textDecoration: 'none',
            }}
          >
            Get Started
          </Button>
        </Container>
      </Body>
    </Html>
  );
}

Send it from an API route:

// src/pages/api/welcome.ts
import type { APIRoute } from 'astro';
import { resend } from '../../lib/resend';
import WelcomeEmail from '../../emails/WelcomeEmail';

export const prerender = false;

export const POST: APIRoute = async ({ request }) => {
  const { name, email } = await request.json();

  const { data, error } = await resend.emails.send({
    from: 'App <welcome@yourdomain.com>',
    to: email,
    subject: `Welcome, ${name}!`,
    react: WelcomeEmail({ name, loginUrl: 'https://yourdomain.com/login' }),
  });

  if (error) {
    return new Response(JSON.stringify({ error }), { status: 400 });
  }

  return new Response(JSON.stringify({ id: data?.id }), { status: 200 });
};

Sending with Astro Actions

The official Resend Astro guide now recommends Astro Actions rather than a hand-rolled API route. Actions give you typed inputs with Zod validation, automatic error handling, and a callable function you can use from both the client and server without writing fetch boilerplate. Actions still need the SSR adapter installed above.

Define the action in src/actions/index.ts:

// src/actions/index.ts
import { defineAction, ActionError } from 'astro:actions';
import { z } from 'astro:schema';
import { resend } from '../lib/resend';

export const server = {
  sendEmail: defineAction({
    accept: 'form',
    input: z.object({
      name: z.string(),
      email: z.string().email(),
      message: z.string(),
    }),
    handler: async ({ name, email, message }) => {
      const { data, error } = await resend.emails.send({
        from: 'Contact Form <onboarding@resend.dev>',
        to: 'you@yourdomain.com',
        replyTo: email,
        subject: `Contact from ${name}`,
        html: `<p><strong>${name}</strong> (${email})</p><p>${message}</p>`,
      });

      if (error) {
        throw new ActionError({
          code: 'INTERNAL_SERVER_ERROR',
          message: error.message,
        });
      }

      return { id: data?.id };
    },
  }),
};

Then point a plain HTML form at the action with astro:actions:

---
// src/pages/contact-action.astro
import { actions } from 'astro:actions';
---

<form method="POST" action={actions.sendEmail}>
  <input type="text" name="name" required />
  <input type="email" name="email" required />
  <textarea name="message" rows="5" required></textarea>
  <button type="submit">Send Message</button>
</form>

Astro routes the submission through the action automatically, so there is no separate /api endpoint to maintain.

Common Errors and Fixes

output: 'hybrid' is not a valid option / build fails on the config: This is the top failure when following older tutorials. The hybrid output value was removed in Astro 5 and is gone in Astro 6. Remove the output key entirely (static is the default), keep the adapter, and add export const prerender = false to each server route. Use output: 'server' only if you want everything rendered on demand by default.

Route returns the page HTML instead of running your handler / POST 404s: An API route or action that does not set export const prerender = false gets pre-rendered at build time and never executes on the server. Add the line to the endpoint file (or set output: 'server').

"You can only send testing emails to your own email address": Without a verified domain, Resend restricts the to address to the email you signed up with. Add and verify a custom domain in the Resend dashboard to send to any address.

Emails going to spam: Configure SPF, DKIM, and DMARC records for your domain. Resend surfaces the exact records to add during the domain verification flow. Also avoid sending from a domain you have not verified.

"from" address rejected / domain not verified: The sender address must use a domain you have verified in Resend, or use onboarding@resend.dev for testing. An unverified domain in the from field returns an error.

React Email template not rendering: Make sure react and react-dom are installed alongside @react-email/components, and pass the component to the react field of resend.emails.send (for example react: WelcomeEmail({ name })). The components need React to render to HTML.

Rate limit hit (HTTP 429): Resend's default limit is 5 requests per second per team. If you send in bursts, batch with resend.batch.send or add a small queue. The free plan also caps you at 100 emails per day and 3,000 per month.

replyTo ignored or "unexpected field" errors: The Node SDK uses camelCase (replyTo, scheduledAt, idempotencyKey), not snake_case. Mixing in reply_to will not behave as expected.

RESEND_API_KEY is undefined at runtime: Reference it as import.meta.env.RESEND_API_KEY from server code and confirm the .env file is present in the deploy environment. Variables without a PUBLIC_ prefix are server-only by design, so this should never appear in client bundles.

Official Docs and Examples

Conclusion

Resend provides a developer-friendly email API that fits naturally into Astro's server-side capabilities. On Astro 5 and 6 the pattern is straightforward: keep the site static, add the Node adapter, and opt the routes that send mail into on-demand rendering with export const prerender = false. Use Astro Actions for typed, validated form handling, and reach for React Email templates when you want polished, responsive designs that render consistently across email clients. Start with the free tier and a verified domain to get production-quality delivery from day one.

Sources

Checked-on 2026-05-29.