| 3 min read

Newsletter Automation with Resend API and Next.js

Resend Next.js newsletter email automation API web development

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 (
    
setEmail(e.target.value)} placeholder="your@email.com" required disabled={status === 'loading'} /> {message &&

{message}

}
); }

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.