Next.js Email Validation
Complete guide to email validation in Next.js
What You'll Learn
- Server-side email validation with API routes
- Client-side validation with React hooks
- Server Actions for email validation
- Form validation with Next.js 14+
- Best practices for production
Prerequisites
- Next.js 14+ installed
- Basic knowledge of Next.js and React
- VerifyForge API key (get free key)
Installation
npm install @verifyforge/sdk
Quick Start: API Route Validation
Step 1: Create API Route
import { NextResponse } from 'next/server';
import { VerifyForge } from '@verifyforge/sdk';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
export async function POST(request: Request) {
try {
const { email } = await request.json();
if (!email) {
return NextResponse.json(
{ error: 'Email is required' },
{ status: 400 }
);
}
const result = await client.validate(email);
return NextResponse.json({
success: true,
data: result.data,
});
} catch (error) {
return NextResponse.json(
{ error: 'Validation failed' },
{ status: 500 }
);
}
}
Step 2: Use in Client Component
'use client';
import { useState } from 'react';
export function EmailForm() {
const [email, setEmail] = useState('');
const [result, setResult] = useState(null);
const [loading, setLoading] = useState(false);
const validateEmail = async () => {
setLoading(true);
try {
const res = await fetch('/api/validate-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const data = await res.json();
setResult(data.data);
} catch (error) {
console.error('Validation failed:', error);
} finally {
setLoading(false);
}
};
return (
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
<button onClick={validateEmail} disabled={loading}>
{loading ? 'Validating...' : 'Validate'}
</button>
{result && (
<div>
<p>Valid: {result.isValid ? '✓' : '✗'}</p>
<p>Reachability: {result.reachability}</p>
</div>
)}
</div>
);
}
Tutorial 1: Server Actions (Next.js 14+)
Step 1: Create Server Action
'use server';
import { VerifyForge } from '@verifyforge/sdk';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
export async function validateEmail(email: string) {
try {
const result = await client.validate(email);
return {
success: true,
data: result.data,
};
} catch (error) {
return {
success: false,
error: 'Validation failed',
};
}
}
export async function validateBulkEmails(emails: string[]) {
try {
const result = await client.validateBulk(emails);
return {
success: true,
data: result.data,
};
} catch (error) {
return {
success: false,
error: 'Bulk validation failed',
};
}
}
Step 2: Use Server Action in Form
'use client';
import { useState, useTransition } from 'react';
import { validateEmail } from '@/app/actions/email';
export function RegistrationForm() {
const [email, setEmail] = useState('');
const [name, setName] = useState('');
const [validationResult, setValidationResult] = useState(null);
const [isPending, startTransition] = useTransition();
const handleEmailBlur = () => {
if (!email) return;
startTransition(async () => {
const result = await validateEmail(email);
if (result.success) {
setValidationResult(result.data);
}
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await validateEmail(email);
if (!result.success || !result.data?.isValid) {
alert('Please enter a valid email address');
return;
}
// Submit form
console.log('Submitting:', { name, email });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
</div>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
onBlur={handleEmailBlur}
required
/>
{isPending && <span>Validating...</span>}
{validationResult && (
<div className={validationResult.isValid ? 'success' : 'error'}>
{validationResult.isValid ? '✓ Valid' : '✗ Invalid'}
</div>
)}
</div>
<button type="submit" disabled={isPending}>
Register
</button>
</form>
);
}
Tutorial 2: Form Validation with react-hook-form
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { validateEmail } from '@/app/actions/email';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email format'),
});
type FormData = z.infer<typeof schema>;
export function FormWithValidation() {
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(schema),
});
const onSubmit = async (data: FormData) => {
// Validate email with VerifyForge
const result = await validateEmail(data.email);
if (!result.success || !result.data?.isValid) {
setError('email', {
type: 'manual',
message: 'This email address is not valid',
});
return;
}
if (result.data.disposable) {
setError('email', {
type: 'manual',
message: 'Temporary email addresses are not allowed',
});
return;
}
if (result.data.reachability === 'invalid') {
setError('email', {
type: 'manual',
message: 'This email cannot receive mail',
});
return;
}
// Submit form
console.log('Form submitted:', data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label htmlFor="name">Name</label>
<input id="name" {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
</div>
<div>
<label htmlFor="email">Email</label>
<input id="email" type="email" {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
</div>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Submitting...' : 'Submit'}
</button>
</form>
);
}
Tutorial 3: Server Component with Edge Runtime
import { VerifyForge } from '@verifyforge/sdk';
import { notFound } from 'next/navigation';
export const runtime = 'edge';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
interface PageProps {
params: Promise<{ email: string }>;
}
export async function generateMetadata({ params }: PageProps) {
const { email } = await params;
return {
title: `Email Validation: ${email}`,
};
}
export default async function VerifyEmailPage({ params }: PageProps) {
const { email } = await params;
const decodedEmail = decodeURIComponent(email);
try {
const result = await client.validate(decodedEmail);
return (
<div>
<h1>Email Validation Results</h1>
<p>Email: {result.data.email}</p>
<p>Valid: {result.data.isValid ? '✓' : '✗'}</p>
<p>Reachability: {result.data.reachability}</p>
<p>Disposable: {result.data.disposable ? 'Yes' : 'No'}</p>
<p>Free Provider: {result.data.freeProvider ? 'Yes' : 'No'}</p>
{result.data.suggestion && (
<p>Did you mean: {result.data.suggestion}?</p>
)}
</div>
);
} catch (error) {
notFound();
}
}
Tutorial 4: Bulk Validation API Route
import { NextResponse } from 'next/server';
import { VerifyForge } from '@verifyforge/sdk';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
export async function POST(request: Request) {
try {
const { emails } = await request.json();
if (!Array.isArray(emails) || emails.length === 0) {
return NextResponse.json(
{ error: 'Emails array is required' },
{ status: 400 }
);
}
if (emails.length > 100) {
return NextResponse.json(
{ error: 'Maximum 100 emails per request' },
{ status: 400 }
);
}
const result = await client.validateBulk(emails);
return NextResponse.json({
success: true,
data: result.data,
summary: {
total: result.data.summary.total,
valid: result.data.summary.valid,
invalid: result.data.summary.invalid,
},
});
} catch (error) {
return NextResponse.json(
{ error: 'Bulk validation failed' },
{ status: 500 }
);
}
}
Tutorial 5: Middleware Validation
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export async function middleware(request: NextRequest) {
// Only validate on specific routes
if (!request.nextUrl.pathname.startsWith('/api/protected')) {
return NextResponse.next();
}
const email = request.headers.get('x-user-email');
if (!email) {
return NextResponse.json(
{ error: 'Email header required' },
{ status: 401 }
);
}
// Validate email
const response = await fetch(`${request.nextUrl.origin}/api/validate-email`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email }),
});
const result = await response.json();
if (!result.success || !result.data?.isValid) {
return NextResponse.json(
{ error: 'Invalid email' },
{ status: 403 }
);
}
return NextResponse.next();
}
export const config = {
matcher: '/api/protected/:path*',
};
Best Practices for Production
1. Environment Variables
VERIFYFORGE_API_KEY=your_api_key_here
2. Rate Limiting
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
const ratelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, '1 m'),
});
export async function checkRateLimit(identifier: string) {
const { success } = await ratelimit.limit(identifier);
return success;
}
import { checkRateLimit } from '@/app/lib/rate-limit';
export async function POST(request: Request) {
const ip = request.headers.get('x-forwarded-for') ?? 'anonymous';
const allowed = await checkRateLimit(ip);
if (!allowed) {
return NextResponse.json(
{ error: 'Rate limit exceeded' },
{ status: 429 }
);
}
// ... rest of validation
}
3. Caching with Next.js
import { unstable_cache } from 'next/cache';
import { VerifyForge } from '@verifyforge/sdk';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
export const validateEmailCached = unstable_cache(
async (email: string) => {
const result = await client.validate(email);
return result.data;
},
['email-validation'],
{
revalidate: 3600, // Cache for 1 hour
tags: ['email-validation'],
}
);
4. Error Handling
import {
AuthenticationError,
InsufficientCreditsError,
ValidationError,
} from '@verifyforge/sdk';
export function handleValidationError(error: unknown) {
if (error instanceof AuthenticationError) {
return { error: 'Invalid API key', status: 401 };
}
if (error instanceof InsufficientCreditsError) {
return { error: 'Insufficient credits', status: 402 };
}
if (error instanceof ValidationError) {
return { error: error.message, status: 400 };
}
return { error: 'Internal server error', status: 500 };
}
Complete Example: Newsletter Subscription
'use client';
import { useState, useTransition } from 'react';
import { subscribeToNewsletter } from '@/app/actions/newsletter';
export function Newsletter() {
const [email, setEmail] = useState('');
const [status, setStatus] = useState<'idle' | 'success' | 'error'>('idle');
const [message, setMessage] = useState('');
const [isPending, startTransition] = useTransition();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
startTransition(async () => {
const result = await subscribeToNewsletter(email);
if (result.success) {
setStatus('success');
setMessage('Successfully subscribed!');
setEmail('');
} else {
setStatus('error');
setMessage(result.error || 'Subscription failed');
}
});
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="your@email.com"
required
disabled={isPending}
/>
<button type="submit" disabled={isPending}>
{isPending ? 'Subscribing...' : 'Subscribe'}
</button>
{message && (
<div className={status === 'success' ? 'success' : 'error'}>
{message}
</div>
)}
</form>
);
}
'use server';
import { VerifyForge } from '@verifyforge/sdk';
const client = new VerifyForge({
apiKey: process.env.VERIFYFORGE_API_KEY!
});
export async function subscribeToNewsletter(email: string) {
try {
// Validate email
const result = await client.validate(email);
if (!result.data.isValid) {
return {
success: false,
error: 'Please enter a valid email address',
};
}
if (result.data.disposable) {
return {
success: false,
error: 'Temporary email addresses are not allowed',
};
}
// Save to database
// await db.newsletter.create({ email });
return { success: true };
} catch (error) {
return {
success: false,
error: 'Subscription failed. Please try again.',
};
}
}
Troubleshooting
Problem: API key not found in environment
Solution: Ensure .env.local exists and contains VERIFYFORGE_API_KEY
Problem: CORS issues
Solution: API routes handle CORS automatically. Use API routes, not client-side calls.
Problem: Slow validation
Solution: Implement caching and use server actions for better performance.
