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.

Next Steps