Angular Email Validation

Complete guide to email validation in Angular

What You'll Learn

  • Install and configure VerifyForge in Angular
  • Create custom form validators
  • Validate emails with reactive forms
  • Build reusable validation services
  • Handle async validation
  • Best practices for production

Prerequisites

  • Angular 16+ installed
  • TypeScript knowledge
  • VerifyForge API key (get free key)

Installation

npm install @verifyforge/sdk

Quick Start

import { VerifyForge } from '@verifyforge/sdk';

const client = new VerifyForge({ apiKey: 'your_api_key' });

const result = await client.validate('user@example.com');

if (result.data.isValid) {
  console.log('✓ Email is valid!');
}

Tutorial 1: Email Validation Service

// src/app/services/email-validation.service.ts
import { Injectable } from '@angular/core';
import { VerifyForge, ValidationResponse } from '@verifyforge/sdk';
import { Observable, from, of } from 'rxjs';
import { catchError, map, shareReplay } from 'rxjs/operators';
import { environment } from '../../environments/environment';

@Injectable({
  providedIn: 'root'
})
export class EmailValidationService {
  private client: VerifyForge;
  private cache = new Map<string, Observable<ValidationResponse>>();

  constructor() {
    this.client = new VerifyForge({
      apiKey: environment.verifyforgeApiKey
    });
  }

  validate(email: string): Observable<ValidationResponse> {
    // Check cache
    if (this.cache.has(email)) {
      return this.cache.get(email)!;
    }

    // Validate and cache
    const validation$ = from(this.client.validate(email)).pipe(
      shareReplay(1),
      catchError(error => {
        console.error('Validation error:', error);
        throw error;
      })
    );

    this.cache.set(email, validation$);

    // Clear cache after 1 hour
    setTimeout(() => this.cache.delete(email), 3600000);

    return validation$;
  }

  validateBulk(emails: string[]): Observable<any> {
    return from(this.client.validateBulk(emails));
  }

  clearCache(): void {
    this.cache.clear();
  }
}

Tutorial 2: Custom Async Validator

// src/app/validators/email-verifyforge.validator.ts
import { AbstractControl, AsyncValidatorFn, ValidationErrors } from '@angular/forms';
import { Observable, of } from 'rxjs';
import { map, catchError, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
import { EmailValidationService } from '../services/email-validation.service';

export function emailVerifyForgeValidator(
  validationService: EmailValidationService,
  options: {
    allowDisposable?: boolean;
    allowRisky?: boolean;
  } = {}
): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }

    return of(control.value).pipe(
      debounceTime(500),
      distinctUntilChanged(),
      switchMap(email => validationService.validate(email)),
      map(result => {
        if (!result.data.isValid) {
          return { invalidEmail: true };
        }

        if (!options.allowDisposable && result.data.disposable) {
          return { disposableEmail: true };
        }

        if (!options.allowRisky && result.data.reachability === 'risky') {
          return { riskyEmail: true };
        }

        if (result.data.reachability === 'invalid') {
          return { unreachableEmail: true };
        }

        return null;
      }),
      catchError(() => of(null)) // Fail gracefully
    );
  };
}

Tutorial 3: Registration Form Component

// src/app/components/registration-form/registration-form.component.ts
import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { emailVerifyForgeValidator } from '../../validators/email-verifyforge.validator';

@Component({
  selector: 'app-registration-form',
  templateUrl: './registration-form.component.html',
  styleUrls: ['./registration-form.component.css']
})
export class RegistrationFormComponent implements OnInit {
  registrationForm!: FormGroup;
  isSubmitting = false;

  constructor(
    private fb: FormBuilder,
    private emailValidation: EmailValidationService
  ) {}

  ngOnInit(): void {
    this.registrationForm = this.fb.group({
      name: ['', [Validators.required]],
      email: [
        '',
        [Validators.required, Validators.email],
        [emailVerifyForgeValidator(this.emailValidation, {
          allowDisposable: false,
          allowRisky: false
        })]
      ],
      password: ['', [Validators.required, Validators.minLength(8)]]
    });
  }

  get email() {
    return this.registrationForm.get('email');
  }

  get emailErrors() {
    const errors = this.email?.errors;
    if (!errors) return null;

    if (errors['required']) return 'Email is required';
    if (errors['email']) return 'Invalid email format';
    if (errors['invalidEmail']) return 'This email is not valid';
    if (errors['disposableEmail']) return 'Temporary emails not allowed';
    if (errors['riskyEmail']) return 'This email appears risky';
    if (errors['unreachableEmail']) return 'This email cannot receive mail';

    return null;
  }

  onSubmit(): void {
    if (this.registrationForm.invalid) {
      return;
    }

    this.isSubmitting = true;

    // Submit form
    const formData = this.registrationForm.value;
    console.log('Submitting:', formData);

    // Reset after submission
    setTimeout(() => {
      this.isSubmitting = false;
      this.registrationForm.reset();
    }, 2000);
  }
}
<!-- src/app/components/registration-form/registration-form.component.html -->
<form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
  <div class="form-group">
    <label for="name">Name</label>
    <input
      id="name"
      type="text"
      formControlName="name"
      class="form-control"
      [class.is-invalid]="registrationForm.get('name')?.invalid && registrationForm.get('name')?.touched"
    />
  </div>

  <div class="form-group">
    <label for="email">Email</label>
    <input
      id="email"
      type="email"
      formControlName="email"
      class="form-control"
      [class.is-invalid]="email?.invalid && email?.touched"
      [class.is-valid]="email?.valid && email?.touched"
    />

    <div *ngIf="email?.pending" class="text-muted">
      <small>Validating email...</small>
    </div>

    <div *ngIf="email?.invalid && email?.touched" class="invalid-feedback">
      {{ emailErrors }}
    </div>

    <div *ngIf="email?.valid && email?.touched" class="valid-feedback">
      ✓ Email is valid
    </div>
  </div>

  <div class="form-group">
    <label for="password">Password</label>
    <input
      id="password"
      type="password"
      formControlName="password"
      class="form-control"
      [class.is-invalid]="registrationForm.get('password')?.invalid && registrationForm.get('password')?.touched"
    />
  </div>

  <button
    type="submit"
    class="btn btn-primary"
    [disabled]="registrationForm.invalid || isSubmitting"
  >
    {{ isSubmitting ? 'Registering...' : 'Register' }}
  </button>
</form>

Tutorial 4: Real-time Email Input Component

// src/app/components/email-input/email-input.component.ts
import { Component, forwardRef } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';
import { EmailValidationService } from '../../services/email-validation.service';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';

@Component({
  selector: 'app-email-input',
  templateUrl: './email-input.component.html',
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => EmailInputComponent),
    multi: true
  }]
})
export class EmailInputComponent implements ControlValueAccessor {
  emailControl = new FormControl('');
  isValidating = false;
  validationResult: any = null;

  onChange: any = () => {};
  onTouched: any = () => {};

  constructor(private emailValidation: EmailValidationService) {
    this.emailControl.valueChanges.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      switchMap(email => {
        if (!email || !email.includes('@')) {
          this.validationResult = null;
          return [];
        }

        this.isValidating = true;
        return this.emailValidation.validate(email);
      })
    ).subscribe(result => {
      this.isValidating = false;
      this.validationResult = result?.data;
    });
  }

  writeValue(value: any): void {
    this.emailControl.setValue(value, { emitEvent: false });
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
    this.emailControl.valueChanges.subscribe(fn);
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
    isDisabled ? this.emailControl.disable() : this.emailControl.enable();
  }
}
<!-- src/app/components/email-input/email-input.component.html -->
<div class="email-input-wrapper">
  <input
    type="email"
    [formControl]="emailControl"
    placeholder="Enter email address"
    class="form-control"
  />

  <div *ngIf="isValidating" class="validation-status">
    ⏳ Validating...
  </div>

  <div *ngIf="!isValidating && validationResult" class="validation-status">
    <span *ngIf="validationResult.isValid" class="text-success">
      ✓ Valid email
    </span>
    <span *ngIf="!validationResult.isValid" class="text-danger">
      ✗ Invalid email
    </span>

    <small *ngIf="validationResult.suggestion" class="text-muted d-block">
      Did you mean {{ validationResult.suggestion }}?
    </small>
  </div>
</div>

Best Practices

1. Environment Configuration

// src/environments/environment.ts
export const environment = {
  production: false,
  verifyforgeApiKey: 'your_api_key_here'
};

2. Error Interceptor

// src/app/interceptors/error.interceptor.ts
import { Injectable } from '@angular/core';
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpErrorResponse } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class ErrorInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return next.handle(req).pipe(
      catchError((error: HttpErrorResponse) => {
        if (error.error?.message?.includes('VerifyForge')) {
          console.error('VerifyForge API error:', error);
        }
        return throwError(() => error);
      })
    );
  }
}

3. Testing

// src/app/services/email-validation.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { EmailValidationService } from './email-validation.service';

describe('EmailValidationService', () => {
  let service: EmailValidationService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(EmailValidationService);
  });

  it('should validate email', (done) => {
    service.validate('test@example.com').subscribe(result => {
      expect(result.data).toBeDefined();
      expect(result.data.email).toBe('test@example.com');
      done();
    });
  });
});

Next Steps