Back to Blog
Technical2026-03-198 min read

Adding Email Validation to Your CI/CD Pipeline

Your test suite checks database schemas, API responses, and UI renders — but does it catch the signup flow accepting 'user@gmai.cm'? Here's how to add email validation testing to your deployment pipeline so bad emails never reach production.

MailSentry·Email Validation API

Adding Email Validation to Your CI/CD Pipeline

TL;DR

  • Add email validation tests to your CI pipeline to catch regressions before they reach production — test against known-invalid, disposable, and typo addresses.
  • Use a pre-deployment smoke test that calls your validation endpoint with a matrix of edge cases to verify all 8 validation layers are responding correctly.
  • Monitor validation response times in staging to catch performance degradation before it affects real users at signup.
Validation in the Pipeline
git push
Unit Tests
Validation Tests
Deploy

Modern applications test everything — database migrations, API contract compliance, component rendering, accessibility, performance budgets. But there is one critical user-facing flow that almost no team tests in their CI pipeline: email validation at signup.

This is how regressions happen. A developer refactors the signup form and accidentally removes the validation call. A dependency update changes the behavior of an email regex. A new checkout flow copies the old signup code but forgets to include the validation middleware. Nobody notices until the bounce rate spikes two weeks later and customer support is drowning in "I never received my confirmation email" tickets.

Adding email validation tests to your CI/CD pipeline takes less than an hour and catches these regressions before they ever reach production.

What to Test: The Validation Matrix

A comprehensive email validation test suite should cover five categories of input. Each category tests a different layer of your validation pipeline:

// test/email-validation.test.ts
const TEST_MATRIX = {
  // Should PASS — valid, deliverable addresses
  valid: [
    "real-user@gmail.com",
    "developer@company.io",
    "name.surname@outlook.com",
  ],

  // Should FAIL — syntactically invalid
  syntax_invalid: [
    "",
    "not-an-email",
    "@no-local-part.com",
    "spaces in@email.com",
    "user@",
    "user@.com",
  ],

  // Should FAIL — domain does not accept email
  domain_invalid: [
    "user@thisdomain-definitely-does-not-exist-xyz.com",
    "user@example.invalid",
  ],

  // Should FLAG — disposable/temporary addresses
  disposable: [
    "anything@guerrillamail.com",
    "test@tempmail.com",
    "throwaway@mailinator.com",
  ],

  // Should SUGGEST — common typos
  typos: [
    { input: "user@gmial.com", expected_suggestion: "user@gmail.com" },
    { input: "user@yaho.com", expected_suggestion: "user@yahoo.com" },
    { input: "user@hotmal.com", expected_suggestion: "user@hotmail.com" },
  ],
};

This matrix ensures that every layer of validation is working: syntax checking, MX record verification, disposable email detection, and typo correction. If any layer breaks or is accidentally removed, the tests catch it immediately.

Unit Tests: Testing Your Validation Logic

Start with unit tests that verify your application's validation logic handles API responses correctly. These tests mock the validation API and run in milliseconds:

// test/signup-validation.test.ts
import { describe, it, expect, vi } from "vitest";
import { validateSignupEmail } from "@/lib/signup";

describe("Signup email validation", () => {
  it("rejects invalid emails", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue(
      new Response(JSON.stringify({
        is_valid: false,
        checks: { syntax: false, mx: false, smtp: false }
      }))
    );

    const result = await validateSignupEmail("not-an-email");
    expect(result.accepted).toBe(false);
    expect(result.error).toContain("invalid");
  });

  it("blocks disposable emails", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue(
      new Response(JSON.stringify({
        is_valid: true,
        is_disposable: true,
        checks: { syntax: true, mx: true, smtp: true }
      }))
    );

    const result = await validateSignupEmail("user@guerrillamail.com");
    expect(result.accepted).toBe(false);
    expect(result.error).toContain("permanent");
  });

  it("suggests typo corrections", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue(
      new Response(JSON.stringify({
        is_valid: false,
        suggested_correction: "user@gmail.com",
        checks: { syntax: true, mx: false }
      }))
    );

    const result = await validateSignupEmail("user@gmial.com");
    expect(result.suggestion).toBe("user@gmail.com");
  });

  it("accepts valid emails", async () => {
    vi.spyOn(global, "fetch").mockResolvedValue(
      new Response(JSON.stringify({
        is_valid: true,
        is_disposable: false,
        risk_score: 12,
        checks: { syntax: true, mx: true, smtp: true }
      }))
    );

    const result = await validateSignupEmail("developer@company.com");
    expect(result.accepted).toBe(true);
  });
});

Integration Tests: Verifying the Full Pipeline

Unit tests verify your code handles responses correctly. Integration tests verify the actual validation API is responding as expected. These tests call the real API and should run in your staging environment or as a pre-deployment gate:

Solve this with MailSentry

8 validation layers, real-time results, sub-50ms response.

Try MailSentry Free →
// test/integration/email-validation.integration.test.ts
import { describe, it, expect } from "vitest";

const API_URL = process.env.API_URL || "https://api.mailsentry.dev";
const API_KEY = process.env.MAILSENTRY_API_KEY;

describe("Email validation API integration", () => {
  it("validates a known-good email", async () => {
    const res = await fetch(`${API_URL}/api/v1/verify`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY!,
      },
      body: JSON.stringify({ email: "test@gmail.com" }),
    });

    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data.checks.syntax).toBe(true);
    expect(data.checks.mx).toBe(true);
  });

  it("detects disposable emails", async () => {
    const res = await fetch(`${API_URL}/api/v1/verify`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY!,
      },
      body: JSON.stringify({ email: "anything@guerrillamail.com" }),
    });

    const data = await res.json();
    expect(data.is_disposable).toBe(true);
  });

  it("responds within acceptable latency", async () => {
    const start = Date.now();
    await fetch(`${API_URL}/api/v1/verify`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-API-Key": API_KEY!,
      },
      body: JSON.stringify({ email: "latency-test@gmail.com" }),
    });
    const elapsed = Date.now() - start;

    // Validation should complete in under 500ms
    expect(elapsed).toBeLessThan(500);
  });
});

The latency test is particularly important. If your validation provider's response time degrades, it directly impacts your signup conversion rate. A 200ms validation call is invisible to users; a 3-second one causes form abandonment. Catching performance regressions in CI prevents silent conversion losses in production.

GitHub Actions: Putting It All Together

Here is a complete GitHub Actions workflow that runs validation tests as part of your deployment pipeline:

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20

      - run: npm ci
      - run: npm run test:unit

      # Integration tests run only on main branch pushes
      - name: Email validation integration tests
        if: github.ref == 'refs/heads/main'
        env:
          MAILSENTRY_API_KEY: $\{{ secrets.MAILSENTRY_API_KEY }}
        run: npm run test:integration

      - name: Deploy to production
        if: github.ref == 'refs/heads/main'
        run: npx vercel --prod --token=$\{{ secrets.VERCEL_TOKEN }}

Unit tests run on every push and pull request — they are fast (under 5 seconds) and catch logic regressions immediately. Integration tests run only on pushes to main, just before deployment, to verify the full validation pipeline is operational.

Pre-Deployment Smoke Tests

For an extra layer of confidence, add a post-deployment smoke test that verifies your production signup flow end-to-end:

// scripts/smoke-test.ts
async function smokeTestSignup() {
  const testCases = [
    { email: "invalid", expectStatus: 400 },
    { email: "user@guerrillamail.com", expectStatus: 400 },
    { email: "valid-user@gmail.com", expectStatus: 200 },
  ];

  for (const tc of testCases) {
    const res = await fetch("https://yourapp.com/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ email: tc.email, password: "test" }),
    });

    if (res.status !== tc.expectStatus) {
      console.error(
        `SMOKE TEST FAILED: ${tc.email} returned ${res.status}, expected ${tc.expectStatus}`
      );
      process.exit(1);
    }
  }

  console.log("All smoke tests passed.");
}

Run this immediately after deployment. If the signup flow is not correctly validating emails in production, you will know within seconds — not weeks.

Common Pitfalls to Avoid

  • Do not hardcode test emails — Use environment variables for API keys and test addresses. Hardcoded keys in test files end up in version control.
  • Do not skip validation in test environments — A common pattern is if (process.env.NODE_ENV === "test") return true. This means your tests never actually verify that validation works. Mock the API response instead of bypassing validation entirely.
  • Do not ignore flaky API tests — If your integration tests fail intermittently, that is a signal that your validation provider has reliability issues. Track the failure rate and set an SLA threshold. MailSentry's sub-50ms average response and 99.9% uptime make integration tests reliable enough to gate deployments on.
  • Do not forget to test the error UI — Your validation logic might correctly reject an invalid email, but if the frontend does not display the error message, the user sees nothing. Add a Playwright or Cypress test that verifies the error message renders.

Key Takeaways

Email validation is a critical user-facing feature that deserves the same testing rigor as your database layer or API contracts. Add unit tests for your validation logic, integration tests for the API, and smoke tests for the production signup flow. Run unit tests on every push, integration tests before deployment, and smoke tests after deployment. This three-layer testing strategy catches regressions at the earliest possible point and ensures that your users never encounter a signup flow that accepts garbage input. The entire setup takes less than an hour and prevents the kind of silent data quality degradation that takes weeks to notice and months to recover from.

Try MailSentry Free

8 validation layers, sub-50ms response, 1,000 checks/month free.

Get Your Free API Key →

Keep Reading

More guides and insights on email validation.

Start validating emails today

1,000 free checks every month. All 8 validation layers included. No credit card needed.