Technology Apr 22, 2026 · 5 min read

I Set Up Email Marketing for My Next.js Business in 30 Minutes (Loops.so)

Every indie hacker who skipped email marketing eventually regrets it. Your X followers, GitHub stars, or Gumroad audience can disappear overnight if the platform changes. Email is the one channel you actually own. Last week I wired email capture into my Next.js template business. From zero subscrib...

DE
DEV Community
by Craftly
I Set Up Email Marketing for My Next.js Business in 30 Minutes (Loops.so)

Every indie hacker who skipped email marketing eventually regrets it. Your X followers, GitHub stars, or Gumroad audience can disappear overnight if the platform changes. Email is the one channel you actually own.

Last week I wired email capture into my Next.js template business. From zero subscribers to a working welcome sequence in under an hour. Here's exactly how.

The Stack

  • Loops.so — email service built for developers, $0 for up to 1,000 contacts
  • Next.js 16.2 — App Router with Route Handlers
  • Vercel — hosting with env var management

Alternatives considered: Kit (ConvertKit), Resend, Mailchimp. Kit is too creator-focused, Resend is transactional-only, Mailchimp is ancient. Loops is the sweet spot for modern indie products.

Step 1: Sign Up and Get the API Key

  1. loops.so → Sign up
  2. Settings → API → Create API Key
  3. Copy

Loops requires a sending domain for delivery. You can build the integration before domain is verified.

Step 2: Store the Key

\`bash

.env.local

LOOPS_API_KEY=
`\

For Vercel: Settings → Environment Variables. Production + Preview + Development.

Step 3: Create the Subscribe Route Handler

\`ts
// src/app/api/subscribe/route.ts
import { NextRequest, NextResponse } from "next/server";

export const runtime = "edge";

const CREATE = "https://app.loops.so/api/v1/contacts/create";
const UPDATE = "https://app.loops.so/api/v1/contacts/update";

export async function POST(req: NextRequest) {
try {
const { email, source = "newsletter" } = await req.json();

if (!email || typeof email !== "string") {
  return NextResponse.json({ error: "Email required" }, { status: 400 });
}

const emailRegex = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/;
if (!emailRegex.test(email)) {
  return NextResponse.json({ error: "Invalid email" }, { status: 400 });
}

const apiKey = process.env.LOOPS_API_KEY;
if (!apiKey) {
  return NextResponse.json({ error: "Not configured" }, { status: 500 });
}

const res = await fetch(CREATE, {
  method: "POST",
  headers: {
    Authorization: \`Bearer \${apiKey}\`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    email, source, subscribed: true, userGroup: "newsletter",
  }),
});

if (!res.ok) {
  const data = await res.json().catch(() => ({}));

  // Duplicate? Update instead (idempotent)
  if (res.status === 409 || data?.message?.includes("already")) {
    const updateRes = await fetch(UPDATE, {
      method: "PUT",
      headers: {
        Authorization: \`Bearer \${apiKey}\`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ email, subscribed: true, source }),
    });

    if (updateRes.ok) {
      return NextResponse.json({ success: true, existing: true });
    }
  }

  return NextResponse.json({ error: "Failed to subscribe" }, { status: 500 });
}

return NextResponse.json({ success: true });

} catch (err) {
console.error("Subscribe error:", err);
return NextResponse.json({ error: "Internal error" }, { status: 500 });
}
}
`\

Key design choices:

  • runtime = "edge"\ for fast global response
  • Idempotent: duplicate signups become updates
  • Email regex validation
  • source\ param to track origin

Step 4: Build the Subscribe Form

\`tsx
"use client";
import { useState } from "react";

export function NewsletterForm() {
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();
if (!email || status === "loading") return;

setStatus("loading");
try {
  const res = await fetch("/api/subscribe", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, source: "homepage-footer" }),
  });

  const data = await res.json();

  if (!res.ok) {
    setStatus("error");
    setMessage(data.error || "Something went wrong");
    return;
  }

  setStatus("success");
  setMessage(data.existing ? "You're already on the list!" : "Thanks!");
  setEmail("");
} catch {
  setStatus("error");
  setMessage("Network error");
}

}

if (status === "success") return

✓ {message};

return (


type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>

{status === "loading" ? "..." : "Subscribe"}


);
}
`\

Drop this anywhere in your layout.

Step 5: Verify Sending Domain

Loops won't actually send until you verify a domain.

  1. Loops → Settings → Domain → Add your domain
  2. Loops generates DNS records:
    • 3x CNAME (DKIM)
    • 1x MX (bounce handling)
    • 1x TXT (SPF)
    • 1x TXT (DMARC)
  3. Add all to your DNS provider
  4. Wait 1-6 hours for propagation
  5. Click Verify

Step 6: Build the Welcome Sequence

In Loops UI:

  1. Create Loop
  2. Trigger: "Contact Added"
  3. Email 1 (immediate): Welcome + gift + CTA
  4. Email 2 (Day 2): Value + subtle discount
  5. Email 3 (Day 5): Story + blog link

Activate. Every new subscriber gets a 3-part welcome automatically.

Step 7: Track Sources

Use the source\ param religiously. After 30 days you'll learn:

  • "homepage-footer" converts 3x better than "blog-modal"
  • One specific blog post drives your best-quality signups
  • Certain channels bring higher LTV subscribers

Gold for optimization.

What I Learned

#1. Email validation is worth it. Users make typos. Loops charges per contact, bad emails compound.

#2. The 409 case matters. Returning users re-subscribing shouldn't see an error.

#3. Welcome sequences should be written before launch. Zero delay between first signup and first email.

#4. Track sources. Free optimization data.

Numbers After Week 1

  • Subscribers: 1 (test)
  • Open rate: N/A (just activated)
  • Domain verification: Processing

Day 4 of the business. The point isn't traffic yet — it's having the system ready so when traffic shows up, I capture it.

Ship It

If you're building a digital product, set up email before launch. Every day you delay is subscribers you'll never get back.

Every Craftly template includes the /api/subscribe\ endpoint + NewsletterForm wired up. Swap in your LOOPS_API_KEY\, deploy, done. Next.js 16.2 + Tailwind v4 + TypeScript, from $19 to $49 or $99 bundle.

Originally published on Craftly.

Check out our premium templates:

Built with Next.js 16, TypeScript, and Tailwind CSS v4.

DE
Source

This article was originally published by DEV Community and written by Craftly.

Read original article on DEV Community
Back to Discover

Reading List