The API route
// app/api/verify-email/route.ts
import { NextRequest, NextResponse } from 'next/server';
const API_KEY = process.env.ROCKETVERIFIER_API_KEY!; // server-side only, never exposed to the client
export async function POST(req: NextRequest) {
const { email } = await req.json();
if (!email) {
return NextResponse.json({ error: 'Email is required' }, { status: 400 });
}
const response = await fetch('https://api.rocketverifier.com/v1/verify', {
method: 'POST',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ email }),
});
if (!response.ok) {
return NextResponse.json({ error: 'Verification temporarily unavailable' }, { status: 502 });
}
const { data } = await response.json();
return NextResponse.json({
status: data.status,
isDisposable: data.domain.disposable,
isCatchAll: data.domain.acceptAll,
});
}
The signup form component
// app/signup/SignupForm.tsx
'use client';
import { useState } from 'react';
type EmailCheck = { status: string; isDisposable: boolean; isCatchAll: boolean } | null;
export default function SignupForm() {
const [email, setEmail] = useState('');
const [check, setCheck] = useState<EmailCheck>(null);
const [checking, setChecking] = useState(false);
async function handleBlur() {
if (!email) return;
setChecking(true);
try {
const res = await fetch('/api/verify-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
setCheck(data);
} finally {
setChecking(false);
}
}
const warning =
check?.status === 'undeliverable'
? "This email address doesn't look deliverable — double-check it."
: check?.isDisposable
? 'Please use a permanent email address.'
: null;
return (
<form method="post" action="/api/signup">
<label htmlFor="email">Email</label>
<input
id="email"
name="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={handleBlur}
aria-invalid={Boolean(warning)}
/>
{checking && <p className="text-sm text-gray-500">Checking...</p>}
{warning && <p className="text-sm text-red-600">{warning}</p>}
<button type="submit" disabled={check?.status === 'undeliverable'}>
Sign up
</button>
</form>
);
}
Testing against the sandbox
Set ROCKETVERIFIER_API_KEY=rv_test_your_sandbox_key in .env.local during development and type an @sandbox.rocketverifier.com address into the form to see the inline warning states render correctly, without needing a live key or spending real credits.
Why this still needs server-side verification too
Client-side, on-blur verification is a UX improvement, not a security boundary — a user can always bypass client-side JavaScript. Always re-verify (or at minimum re-check disposable/undeliverable status) in your actual signup-processing endpoint before creating an account, the same way the Express/Node recipe does server-side.