Newsletter Automation with Resend API and Next.js
Why I Chose Resend Over Mailchimp
For my portfolio site newsletter, I wanted something developer-friendly with a clean API, reasonable pricing, and no bloated visual editor. Resend checked every box. It is built by developers for developers, the API is clean, and it integrates perfectly with Next.js through their React email components.
Here is the complete newsletter automation system I built.
Setting Up Resend
npm install resend @react-email/components
API Route for Sending
// app/api/newsletter/send/route.ts
import { Resend } from 'resend';
import { NextRequest, NextResponse } from 'next/server';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(req: NextRequest) {
const { subject, html, audience_id } = await req.json();
// Validate API key for internal use
const apiKey = req.headers.get('x-api-key');
if (apiKey !== process.env.INTERNAL_API_KEY) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const data = await resend.emails.send({
from: 'Steve Light ',
to: audience_id ? undefined : [],
subject,
html,
headers: {
'List-Unsubscribe': ''
}
});
return NextResponse.json({ success: true, id: data.id });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to send' },
{ status: 500 }
);
}
}
Subscriber Management
// app/api/newsletter/subscribe/route.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function POST(req: NextRequest) {
const { email, name } = await req.json();
if (!email || !email.includes('@')) {
return NextResponse.json(
{ error: 'Valid email required' },
{ status: 400 }
);
}
try {
// Add to Resend audience
await resend.contacts.create({
email,
firstName: name?.split(' ')[0] || '',
lastName: name?.split(' ').slice(1).join(' ') || '',
audienceId: process.env.RESEND_AUDIENCE_ID,
unsubscribed: false
});
// Send welcome email
await resend.emails.send({
from: 'Steve Light ',
to: email,
subject: 'Welcome to the AI Engineering Newsletter',
html: getWelcomeEmailHtml(name)
});
return NextResponse.json({ success: true });
} catch (error: any) {
if (error.message?.includes('already exists')) {
return NextResponse.json(
{ error: 'Already subscribed' },
{ status: 409 }
);
}
return NextResponse.json(
{ error: 'Subscription failed' },
{ status: 500 }
);
}
}
The Subscribe Form Component
// components/SubscribeForm.tsx
'use client';
import { useState } from 'react';
export default function SubscribeForm() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [message, setMessage] = useState('');
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setStatus('loading');
try {
const res = await fetch('/api/newsletter/subscribe', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email })
});
const data = await res.json();
if (res.ok) {
setStatus('success');
setMessage('Check your inbox for a welcome email!');
setEmail('');
} else {
setStatus('error');
setMessage(data.error || 'Something went wrong');
}
} catch {
setStatus('error');
setMessage('Network error. Please try again.');
}
}
return (
);
}
Email Templates with React
// emails/WeeklyDigest.tsx import { Html, Head, Body, Container, Section, Heading, Text, Link, Hr } from '@react-email/components'; interface DigestProps { posts: Array<{ title: string; url: string; excerpt: string }>; weekNumber: number; } export default function WeeklyDigest({ posts, weekNumber }: DigestProps) { return (); } AI Engineering Weekly #{weekNumber} Here is what I published this week: {posts.map((post, i) => ({post.title} ))}{post.excerpt}
You received this because you subscribed at stevecv.com. Unsubscribe
Automated Weekly Digest
// scripts/send-digest.ts
import { render } from '@react-email/render';
import WeeklyDigest from '../emails/WeeklyDigest';
async function sendWeeklyDigest() {
// Get this week's published posts
const posts = await getRecentPosts(7);
if (posts.length === 0) {
console.log('No new posts this week, skipping digest');
return;
}
const weekNumber = getWeekNumber();
const html = render(WeeklyDigest({ posts, weekNumber }));
const response = await fetch('https://stevecv.com/api/newsletter/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.INTERNAL_API_KEY
},
body: JSON.stringify({
subject: `AI Engineering Weekly #${weekNumber}`,
html,
audience_id: process.env.RESEND_AUDIENCE_ID
})
});
console.log('Digest sent:', await response.json());
}
sendWeeklyDigest();
Unsubscribe Handling
Respecting unsubscribes is both legally required and good practice. I handle it with a simple API route:
// app/api/newsletter/unsubscribe/route.ts
export async function POST(req: NextRequest) {
const { email } = await req.json();
await resend.contacts.update({
email,
audienceId: process.env.RESEND_AUDIENCE_ID,
unsubscribed: true
});
return NextResponse.json({ success: true });
}
Monitoring and Analytics
Resend provides delivery webhooks that I use to track open rates, click rates, and bounces. I store these events in a simple database for analysis.
- Track delivery rate to ensure emails are not going to spam
- Monitor bounce rate and auto-remove hard bounces
- Measure open rates per digest to understand what topics resonate
This entire newsletter system runs on Resend's free tier for up to 3,000 emails per month. For a personal portfolio newsletter, that is more than enough. The developer experience with Resend and Next.js is excellent, and the React email components make template creation enjoyable rather than painful.