How to Integrate SendGrid with Astro: Complete Guide
Step-by-step guide to integrating SendGrid with your Astro website. Setup, configuration, and best practices.
How to Integrate SendGrid with Astro: Complete Guide
SendGrid, now part of Twilio, is one of the most widely used email delivery platforms, handling transactional emails, marketing campaigns, and email validation for businesses of all sizes. It offers a robust API, dynamic email templates, and detailed delivery analytics. Integrating SendGrid with Astro lets you send emails from contact forms, user notifications, and automated workflows directly from your server-rendered Astro application.
This guide walks through setting up SendGrid in an Astro project, sending emails via the API, using dynamic templates, and handling form submissions. It is written against Astro 6 (the current major release), where static output is the default and on-demand rendering is opted into per route.
Versions verified on 2026-05-29 against the npm registry and the official Astro and Twilio SendGrid docs:
astro@6.4.2,@astrojs/node@10.1.2,@sendgrid/mail@8.1.6. See the Sources section at the end. Pin these if you want a reproducible build.
Prerequisites
Before starting, make sure you have:
- Node.js 22.12.0 or newer installed. Astro 6 declares
engines.nodeas>=22.12.0, so older runtimes will refuse to install or build. - An existing Astro project on version 6 (the current major). Because email sending runs server-side, the page or endpoint that sends mail must be rendered on demand (covered under Configuration).
- A SendGrid account (free tier at sendgrid.com)
- A SendGrid API key with Mail Send permissions
- A verified sender identity (single sender or domain authentication). SendGrid will reject any send from an unverified
fromaddress.
Installation
Install the official SendGrid Node.js library:
npm install @sendgrid/mail
The SendGrid Node.js SDK is published as a monorepo of scoped packages. For sending mail you only need @sendgrid/mail. The current version at the time of writing is 8.1.6.
Add the Node adapter so Astro can render pages on demand. This command installs @astrojs/node and wires it into your config automatically:
npx astro add node
If you prefer to install the adapter by hand, run npm install @astrojs/node and add the adapter entry to your config yourself (shown below). The current adapter version is 10.1.2, and it requires Astro ^6.3.0 as a peer dependency.
Configuration
Environment Variables
Add your SendGrid API key and sender email to .env:
SENDGRID_API_KEY=SG.your_api_key_here
SENDGRID_FROM_EMAIL=noreply@yourdomain.com
SENDGRID_FROM_NAME=Your Site Name
Setting Up the SendGrid Client
Create a utility file that initializes the SendGrid client:
// src/lib/sendgrid.ts
import sgMail from '@sendgrid/mail';
sgMail.setApiKey(import.meta.env.SENDGRID_API_KEY);
export { sgMail };
export const defaultFrom = {
email: import.meta.env.SENDGRID_FROM_EMAIL,
name: import.meta.env.SENDGRID_FROM_NAME,
};
Astro Config
Astro 5 removed output: 'hybrid'. The official v5 upgrade guide states that v5 "merges the output: 'hybrid' and output: 'static' configurations into one single configuration (now called 'static') that works the same way as the previous hybrid option," so static is the default and you no longer set an output value to mix static and on-demand pages. Adding the adapter is enough. Any page or endpoint that needs server rendering opts in with export const prerender = false, while everything else stays statically generated.
// astro.config.mjs
import { defineConfig } from 'astro/config';
import node from '@astrojs/node';
export default defineConfig({
// No `output` key needed. `static` is the default and supports
// per-route on-demand rendering via `export const prerender = false`.
adapter: node({ mode: 'standalone' }),
});
If you want every route server-rendered by default instead, set output: 'server' (still a valid value in Astro 6) and opt individual static pages back out with export const prerender = true. For a blog with a single contact endpoint, leaving the default static and marking just that one route on demand is the leaner choice.
The Node adapter supports two modes. standalone (used above) builds a server that starts itself when you run the entry module, which is the simplest way to deploy. middleware builds output you mount inside another Node server such as Express or Fastify.
Common Patterns
Sending a Basic Email
Create an API endpoint for sending emails:
With the default static output, an endpoint that runs at request time must opt out of prerendering with export const prerender = false. Without it, Astro tries to build the route at compile time and the live POST handler never runs.
// src/pages/api/send-email.ts
import type { APIRoute } from 'astro';
import { sgMail, defaultFrom } from '../../lib/sendgrid';
export const prerender = false; // render on demand so POST runs at request time
export const POST: APIRoute = async ({ request }) => {
const body = await request.json();
try {
await sgMail.send({
to: body.to,
from: defaultFrom,
subject: body.subject,
text: body.message,
html: `<p>${body.message}</p>`,
});
return new Response(JSON.stringify({ success: true }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} catch (error: any) {
const message = error.response?.body?.errors?.[0]?.message || 'Failed to send email';
return new Response(JSON.stringify({ error: message }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
};
Contact Form Implementation
Build a server-rendered contact page:
---
// src/pages/contact.astro
export const prerender = false;
let submitted = false;
let errorMsg = '';
if (Astro.request.method === 'POST') {
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 { sgMail, defaultFrom } = await import('../lib/sendgrid');
try {
await sgMail.send({
to: 'you@yourdomain.com',
from: defaultFrom,
replyTo: email,
subject: `Contact: ${name}`,
html: `
<h2>New Contact Submission</h2>
<p><strong>From:</strong> ${name} (${email})</p>
<hr />
<p>${message.replace(/\n/g, '<br />')}</p>
`,
});
submitted = true;
} catch {
errorMsg = 'Failed to send your message. Please try again later.';
}
}
---
<html>
<body>
<h1>Contact</h1>
{submitted ? (
<p>Thank you! Your message has been sent.</p>
) : (
<form method="POST">
{errorMsg && <p style="color: red;">{errorMsg}</p>}
<label>Name <input type="text" name="name" required /></label>
<label>Email <input type="email" name="email" required /></label>
<label>Message <textarea name="message" rows="6" required></textarea></label>
<button type="submit">Send</button>
</form>
)}
</body>
</html>
Using Dynamic Templates
SendGrid's Dynamic Templates let you design emails in the SendGrid dashboard and pass data variables from your code:
// src/pages/api/welcome.ts
import type { APIRoute } from 'astro';
import { sgMail, defaultFrom } from '../../lib/sendgrid';
export const prerender = false;
export const POST: APIRoute = async ({ request }) => {
const { email, name } = await request.json();
try {
await sgMail.send({
to: email,
from: defaultFrom,
templateId: 'd-your_template_id_here',
dynamicTemplateData: {
name: name,
login_url: 'https://yourdomain.com/login',
current_year: new Date().getFullYear(),
},
});
return new Response(JSON.stringify({ success: true }), { status: 200 });
} catch (error: any) {
return new Response(JSON.stringify({ error: 'Send failed' }), { status: 500 });
}
};
Create the template in SendGrid Dashboard > Email API > Dynamic Templates. Use Handlebars syntax like {{name}} in the template design to reference the variables you pass.
Sending to Multiple Recipients
await sgMail.send({
to: ['user1@example.com', 'user2@example.com'],
from: defaultFrom,
subject: 'Update Notification',
html: '<p>This is an update.</p>',
isMultiple: true, // Sends individual emails, not one with all addresses visible
});
Adding Attachments
import { readFileSync } from 'node:fs';
await sgMail.send({
to: 'user@example.com',
from: defaultFrom,
subject: 'Your Report',
html: '<p>Your report is attached.</p>',
attachments: [
{
content: readFileSync('/path/to/report.pdf').toString('base64'),
filename: 'report.pdf',
type: 'application/pdf',
disposition: 'attachment',
},
],
});
Common Errors and Fixes
output: 'hybrid' is not a valid value: If you copied an older tutorial, your build will fail because Astro 5 removed the hybrid option and merged it into static. Delete the output: 'hybrid' line entirely. The default static mode already supports mixed static and on-demand pages, and you opt routes into server rendering with export const prerender = false. See the Astro v5 upgrade guide in Sources.
Form POST or API endpoint returns the static HTML instead of running: With the default static output, a route is prerendered unless it declares export const prerender = false. If your send-email.ts endpoint or contact.astro page never reaches the SendGrid call, confirm export const prerender = false is present at the top of that file and that an adapter is configured. On-demand rendering requires an adapter, per the Astro on-demand rendering guide.
Adapter peer dependency mismatch: @astrojs/node@10.1.2 declares a peer dependency of astro@^6.3.0. If npm warns about an unmet peer dependency, upgrade Astro to 6.3 or newer rather than pinning an older adapter.
Install fails with an engine error: Astro 6 requires Node.js >=22.12.0 (declared in its engines field). On an older Node version the install or build aborts. Upgrade Node, or use a version manager like nvm to switch.
import.meta.env.SENDGRID_API_KEY is undefined: Variables in .env are only exposed to server-side code. Make sure the SendGrid client is imported in a server context (an API route or a page with prerender = false), not in a client-side <script>. Astro does not bundle secret env vars into the client unless they are prefixed PUBLIC_, which you must never do for an API key.
"Forbidden" or 403 error: Your API key may not have Mail Send permissions. Go to SendGrid Dashboard > Settings > API Keys, and make sure your key has at least "Mail Send" access. Full Access keys also work.
Emails not arriving: Check your SendGrid Activity Feed (Email API > Activity) for bounce, block, or drop events. Common causes are unverified sender identity, recipient domain blocking, or content flagged as spam.
"Sender identity not verified" error: You must verify either a single sender email or authenticate your entire domain before sending. SendGrid rejects any send whose from address is not verified. Go to Settings > Sender Authentication in the SendGrid dashboard.
HTML email rendering issues: Different email clients render HTML differently. Use inline CSS styles instead of <style> blocks. SendGrid's Dynamic Templates include a visual editor that handles cross-client compatibility.
Module import error in production: Some deployment platforms require the SendGrid package to be listed in dependencies, not devDependencies. Check your package.json and move it if needed.
Conclusion
SendGrid provides a battle-tested email delivery infrastructure that integrates cleanly with Astro's API routes and on-demand rendering. The combination of the Node.js SDK for programmatic sending and Dynamic Templates for visual email design covers most email use cases. Start the SendGrid free tier for development and contact forms, verify your sending domain early, and use the Activity Feed to monitor deliverability as you scale.
Official Docs and Examples
- Twilio SendGrid Node.js quickstart (install, API key setup, first send): https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-nodejs
- Official SendGrid Node.js SDK repository, including the
@sendgrid/mailpackage and runnable examples in itsuse-casesfolder: https://github.com/sendgrid/sendgrid-nodejs @sendgrid/mailon npm: https://www.npmjs.com/package/@sendgrid/mail- Astro on-demand rendering guide (adapters,
output, per-routeprerender): https://docs.astro.build/en/guides/on-demand-rendering/ - Astro Node adapter (
@astrojs/node) docs: https://docs.astro.build/en/guides/integrations-guide/node/ - Astro v5 upgrade guide (the
hybridtostaticchange): https://docs.astro.build/en/guides/upgrade-to/v5/
Sources
All versions and configuration facts below were checked on 2026-05-29.
astro@6.4.2andengines.node >=22.12.0, from the npm registry: https://registry.npmjs.org/astro/latest@astrojs/node@10.1.2with peer dependencyastro@^6.3.0, from the npm registry: https://registry.npmjs.org/@astrojs/node/latest@sendgrid/mail@8.1.6, from the npm registry: https://registry.npmjs.org/@sendgrid/mail/latest- Astro on-demand rendering:
staticis the default,serverrequires an adapter, per-route opt-in withexport const prerender = false. https://docs.astro.build/en/guides/on-demand-rendering/ - Removal of
output: 'hybrid'and the merge intostaticin Astro 5. https://docs.astro.build/en/guides/upgrade-to/v5/ @astrojs/nodeinstall command (npx astro add node),standaloneandmiddlewaremodes, andadapter: node({ mode: 'standalone' })config. https://docs.astro.build/en/guides/integrations-guide/node/- SendGrid Node.js install command (
npm install @sendgrid/mail) andsgMail.setApiKey(...)usage. https://www.twilio.com/docs/sendgrid/for-developers/sending-email/quickstart-nodejs - Official SendGrid Node.js SDK repository. https://github.com/sendgrid/sendgrid-nodejs
Related Articles
How to Use Algolia with Astro: Complete Guide
Step-by-step guide to integrating Algolia with your Astro website.
How to Integrate Auth0 with Astro: Complete Guide
Step-by-step guide to integrating Auth0 with your Astro website. Setup, configuration, and best practices.
How to Use AWS Amplify with Astro: Complete Guide
Step-by-step guide to integrating AWS Amplify with your Astro website.