Home / Blog / EU VAT Validation in Node.js: Complete Tutorial with Error Handling

Tutorial14 min read

EU VAT Validation in Node.js: Complete Tutorial with Error Handling

Copy-paste Node.js / TypeScript implementation for EU VAT validation with proper error handling, caching, and test coverage.


This is a complete Node.js and TypeScript tutorial for EU VAT number validation using the TaxID API. It covers everything from the initial fetch call to a production-ready service class with proper error handling, caching, Express.js middleware, Next.js API route integration, and a full Vitest test suite. If you read the complete EU VAT validation guide first, this tutorial is the Node.js implementation of everything covered there.

Prerequisites and Setup

You need a TaxID API key — the free tier (100 validations/month) is sufficient for this tutorial. Get one at taxid.dev/signup in under two minutes, no credit card required. Store your key in an environment variable: TAXID_API_KEY=vat_xxxx. Never hardcode it or commit it to version control.

bash
# Install no extra dependencies — Node.js 18+ has built-in fetch
# For Node.js 16, add node-fetch: npm install node-fetch

# Add to your .env file:
TAXID_API_KEY=vat_xxxxxxxxxxxxxxxx

# Verify you have Node.js 18+:
node --version  # should be v18 or higher

TypeScript Types for the API Response

Define the response type before writing any implementation code. Strong types make the rest of the implementation safer and give you IDE autocompletion throughout. The status field is the most important — it is a discriminated union that drives all downstream logic.

typescripttypes/vat.ts
export type VatStatus =
  | 'active'
  | 'inactive'
  | 'format_invalid'
  | 'service_unavailable';

export interface VatApiResponse {
  valid: boolean;
  status: VatStatus;
  vat: string;
  country_code: string;
  company_name: string | null;
  address: string | null;
  cached: boolean;
  request_id: string;
}

export interface VatValidationResult {
  isValid: boolean;
  isUnavailable: boolean;
  isFormatInvalid: boolean;
  companyName: string | null;
  address: string | null;
  status: VatStatus;
  cached: boolean;
  requestId: string;
  raw: VatApiResponse;
}

Basic Validation Function

The minimal implementation: a single async function that validates one VAT number and returns a typed result. This is the building block for everything else in this tutorial.

typescriptlib/vat.ts
import type { VatApiResponse, VatValidationResult } from '../types/vat';

const BASE_URL = 'https://taxid.dev/api/v1';

export async function validateVat(
  vatNumber: string
): Promise<VatValidationResult> {
  // Extract country code from first 2 characters
  const country = vatNumber.slice(0, 2).toUpperCase();
  const normalised = vatNumber.replace(/\s/g, '').toUpperCase();

  const response = await fetch(
    `${BASE_URL}/validate/${country}/${normalised}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.TAXID_API_KEY}`,
        Accept: 'application/json',
      },
      signal: AbortSignal.timeout(5000), // 5-second timeout
    }
  );

  if (!response.ok) {
    throw new Error(
      `TaxID API error: ${response.status} ${response.statusText}`
    );
  }

  const data = (await response.json()) as VatApiResponse;

  return {
    isValid: data.valid,
    isUnavailable: data.status === 'service_unavailable',
    isFormatInvalid: data.status === 'format_invalid',
    companyName: data.company_name,
    address: data.address,
    status: data.status,
    cached: data.cached,
    requestId: data.request_id,
    raw: data,
  };
}

Handling All Error States

Every VAT validation call can return four distinct status values, each requiring different application logic. Getting this right is the difference between a robust integration and one that silently fails in production. Here is a complete handler for all four cases:

typescriptlib/vat-handler.ts
import { validateVat } from './vat';

type CheckoutVatOutcome =
  | { outcome: 'valid'; companyName: string | null; address: string | null }
  | { outcome: 'invalid'; reason: 'inactive' | 'format_invalid' }
  | { outcome: 'unavailable' };

export async function handleCheckoutVat(
  vatNumber: string
): Promise<CheckoutVatOutcome> {
  let result;

  try {
    result = await validateVat(vatNumber);
  } catch {
    // Network error or timeout — treat as unavailable
    return { outcome: 'unavailable' };
  }

  switch (result.status) {
    case 'active':
      return {
        outcome: 'valid',
        companyName: result.companyName,
        address: result.address,
      };

    case 'inactive':
      return { outcome: 'invalid', reason: 'inactive' };

    case 'format_invalid':
      return { outcome: 'invalid', reason: 'format_invalid' };

    case 'service_unavailable':
      // VIES is down — allow through, queue for re-validation
      return { outcome: 'unavailable' };
  }
}

Building a Reusable VATValidator Class

For larger applications, a class-based service is cleaner than standalone functions. The VATValidator class wraps the API call with in-memory caching, configurable timeout, and retry-on-network-error logic. Use this as a singleton across your application.

typescriptlib/VatValidator.ts
import type { VatValidationResult } from '../types/vat';
import { validateVat } from './vat';

interface VatValidatorOptions {
  cacheMaxAge?: number;  // milliseconds, default 23h
  timeout?: number;      // milliseconds, default 5000
  retries?: number;      // on network errors, default 1
}

export class VatValidator {
  private cache = new Map<string, { result: VatValidationResult; cachedAt: number }>();
  private maxAge: number;
  private timeout: number;
  private retries: number;

  constructor(options: VatValidatorOptions = {}) {
    this.maxAge = options.cacheMaxAge ?? 23 * 60 * 60 * 1000; // 23h
    this.timeout = options.timeout ?? 5000;
    this.retries = options.retries ?? 1;
  }

  async validate(vatNumber: string): Promise<VatValidationResult> {
    const key = vatNumber.toUpperCase().replace(/\s/g, '');

    // Return in-memory cache if fresh
    const cached = this.cache.get(key);
    if (cached && Date.now() - cached.cachedAt < this.maxAge) {
      return { ...cached.result, cached: true };
    }

    let lastError: Error | null = null;
    for (let attempt = 0; attempt <= this.retries; attempt++) {
      try {
        const result = await validateVat(key);

        // Only cache definitive results (not service_unavailable)
        if (result.status !== 'service_unavailable') {
          this.cache.set(key, { result, cachedAt: Date.now() });
        }

        return result;
      } catch (err) {
        lastError = err as Error;
        // Wait 500ms before retry
        if (attempt < this.retries) {
          await new Promise(r => setTimeout(r, 500));
        }
      }
    }

    // All retries exhausted — return unavailable
    return {
      isValid: false,
      isUnavailable: true,
      isFormatInvalid: false,
      companyName: null,
      address: null,
      status: 'service_unavailable',
      cached: false,
      requestId: '',
      raw: {} as never,
    };
  }

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

// Singleton export — import this throughout your app
export const vatValidator = new VatValidator();

Express.js Middleware

For Express.js applications, a VAT validation middleware that runs on order creation or B2B signup routes keeps validation logic out of your route handlers. The middleware attaches the validation result to req so downstream handlers can use it.

typescriptmiddleware/validateVat.ts
import { Request, Response, NextFunction } from 'express';
import { vatValidator } from '../lib/VatValidator';

declare global {
  namespace Express {
    interface Request {
      vatResult?: {
        status: string;
        companyName: string | null;
        address: string | null;
      };
    }
  }
}

export async function requireVatValidation(
  req: Request,
  res: Response,
  next: NextFunction
) {
  const vatNumber = req.body?.vatNumber as string | undefined;

  // No VAT number — B2C customer, skip validation
  if (!vatNumber) return next();

  const result = await vatValidator.validate(vatNumber);

  if (result.status === 'inactive' || result.status === 'format_invalid') {
    return res.status(422).json({
      error: 'invalid_vat_number',
      status: result.status,
      message:
        result.status === 'format_invalid'
          ? 'VAT number format is invalid. Expected format: CC + digits (e.g. DE123456789).'
          : 'VAT number is not currently registered in the EU VIES system.',
    });
  }

  // Attach result (valid or unavailable) for downstream handlers
  req.vatResult = {
    status: result.status,
    companyName: result.companyName,
    address: result.address,
  };

  next();
}

// Usage:
// router.post('/orders', requireVatValidation, createOrderHandler);

Next.js API Route

For Next.js applications, use an App Router API route to expose VAT validation to your frontend without leaking the API key. The route validates the number server-side and returns the result.

typescriptapp/api/validate-vat/route.ts
// app/api/validate-vat/route.ts (Next.js 14 App Router)
import { vatValidator } from '@/lib/VatValidator';

export async function POST(request: Request) {
  const body = await request.json().catch(() => null);

  if (!body?.vatNumber || typeof body.vatNumber !== 'string') {
    return Response.json(
      { error: 'vat_number_required' },
      { status: 400 }
    );
  }

  const result = await vatValidator.validate(body.vatNumber);

  // Never expose raw API response to frontend
  return Response.json({
    valid: result.isValid,
    status: result.status,
    companyName: result.companyName,
    cached: result.cached,
  });
}

In-Memory vs Redis Caching

The VATValidator class above uses an in-memory Map for caching. This works well for single-instance applications but has two limitations: the cache is lost on process restart, and it does not share state across multiple application instances in a horizontally scaled deployment. For production applications with more than one instance, use Redis for the cache instead.

typescriptlib/VatValidatorRedis.ts
import { Redis } from '@upstash/redis';
import { validateVat } from './vat';
import type { VatValidationResult } from '../types/vat';

const redis = new Redis({
  url: process.env.UPSTASH_REDIS_REST_URL!,
  token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});

const CACHE_TTL = 23 * 60 * 60; // 23 hours in seconds

export async function validateVatWithRedis(
  vatNumber: string
): Promise<VatValidationResult> {
  const key = `vat:${vatNumber.toUpperCase().replace(/\s/g, '')}`;

  // Check Redis cache first
  const cached = await redis.get<VatValidationResult>(key);
  if (cached) return { ...cached, cached: true };

  // Cache miss — call the API
  const result = await validateVat(vatNumber);

  // Only cache definitive results (active or inactive)
  if (result.status === 'active' || result.status === 'inactive') {
    await redis.setex(key, CACHE_TTL, result);
  }

  return result;
}

Full Vitest Test Suite

Testing all four response states — active, inactive, format_invalid, service_unavailable — and the network error case ensures your integration is correct before it reaches production. Mock fetch at the module level so tests are deterministic and do not make real API calls.

typescriptlib/vat.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { validateVat } from './vat';
import { VatValidator } from './VatValidator';

const mockFetch = vi.spyOn(global, 'fetch');

const makeRes = (data: object) =>
  Promise.resolve({ ok: true, json: async () => data } as Response);

beforeEach(() => vi.clearAllMocks());

describe('validateVat', () => {
  it('returns isValid: true for an active number', async () => {
    mockFetch.mockImplementationOnce(() =>
      makeRes({ valid: true, status: 'active', company_name: 'Acme GmbH',
                address: 'Berlin', cached: false, request_id: 'r1',
                vat: 'DE123456789', country_code: 'DE' })
    );
    const r = await validateVat('DE123456789');
    expect(r.isValid).toBe(true);
    expect(r.companyName).toBe('Acme GmbH');
    expect(r.isUnavailable).toBe(false);
  });

  it('returns isValid: false for an inactive number', async () => {
    mockFetch.mockImplementationOnce(() =>
      makeRes({ valid: false, status: 'inactive', company_name: null,
                address: null, cached: false, request_id: 'r2',
                vat: 'DE000000000', country_code: 'DE' })
    );
    const r = await validateVat('DE000000000');
    expect(r.isValid).toBe(false);
    expect(r.status).toBe('inactive');
    expect(r.isUnavailable).toBe(false); // MUST be false
  });

  it('returns isUnavailable: true — NOT isValid: false — when VIES is down', async () => {
    mockFetch.mockImplementationOnce(() =>
      makeRes({ valid: false, status: 'service_unavailable', company_name: null,
                address: null, cached: false, request_id: 'r3',
                vat: 'DE123456789', country_code: 'DE' })
    );
    const r = await validateVat('DE123456789');
    expect(r.isValid).toBe(false);
    expect(r.isUnavailable).toBe(true); // Key assertion
    expect(r.status).toBe('service_unavailable');
  });

  it('returns format_invalid for a malformed VAT number', async () => {
    mockFetch.mockImplementationOnce(() =>
      makeRes({ valid: false, status: 'format_invalid', company_name: null,
                address: null, cached: false, request_id: 'r4',
                vat: 'DE123', country_code: 'DE' })
    );
    const r = await validateVat('DE123');
    expect(r.isFormatInvalid).toBe(true);
  });

  it('throws on non-200 API responses', async () => {
    mockFetch.mockImplementationOnce(() =>
      Promise.resolve({ ok: false, status: 429, statusText: 'Too Many Requests' } as Response)
    );
    await expect(validateVat('DE123456789')).rejects.toThrow('429');
  });
});

describe('VatValidator in-memory cache', () => {
  it('returns cached result on second call', async () => {
    mockFetch.mockImplementation(() =>
      makeRes({ valid: true, status: 'active', company_name: 'Cached Corp',
                address: null, cached: false, request_id: 'r5',
                vat: 'DE123456789', country_code: 'DE' })
    );
    const validator = new VatValidator();
    await validator.validate('DE123456789');
    const r = await validator.validate('DE123456789');
    expect(mockFetch).toHaveBeenCalledTimes(1); // Only one API call
    expect(r.cached).toBe(true);
  });

  it('does not cache service_unavailable responses', async () => {
    mockFetch.mockImplementation(() =>
      makeRes({ valid: false, status: 'service_unavailable', company_name: null,
                address: null, cached: false, request_id: 'r6',
                vat: 'DE123456789', country_code: 'DE' })
    );
    const validator = new VatValidator();
    await validator.validate('DE123456789');
    await validator.validate('DE123456789');
    expect(mockFetch).toHaveBeenCalledTimes(2); // Two API calls — no caching
  });
});

Integrating with Prisma and a Database

For full-stack applications that need to persist validation results for audit compliance, here is how to integrate the VATValidator with Prisma. The pattern stores every validation call with a timestamp and the full response fields — this is the audit trail you need for zero-rate invoice compliance. See the complete EU VAT guide for the storage schema rationale and GDPR considerations.

typescriptlib/vat-with-db.ts
import { vatValidator } from './VatValidator';
import { prisma } from './prisma';

export async function validateAndStore(
  vatNumber: string,
  customerId: string,
  source: 'signup' | 'checkout' | 'revalidation'
) {
  const result = await vatValidator.validate(vatNumber);

  await prisma.vatValidation.create({
    data: {
      customerId,
      vatNumber: result.raw.vat ?? vatNumber,
      countryCode: result.raw.country_code ?? vatNumber.slice(0, 2).toUpperCase(),
      status: result.status,
      isValid: result.isValid,
      companyName: result.companyName,
      address: result.address,
      requestId: result.requestId,
      checkedAt: new Date(),
      source,
      cached: result.cached,
    },
  });

  // Update customer record
  if (result.status === 'active') {
    await prisma.customer.update({
      where: { id: customerId },
      data: {
        vatNumber: result.raw.vat,
        vatStatus: 'b2b',
        companyName: result.companyName,
        vatValidatedAt: new Date(),
      },
    });
  } else if (result.status === 'inactive') {
    await prisma.customer.update({
      where: { id: customerId },
      data: { vatStatus: 'b2c', vatInvalidatedAt: new Date() },
    });
  }
  // service_unavailable: leave status unchanged, queue re-validation

  return result;
}

Rate Limiting and Quota Management

Your TaxID plan has a monthly quota. Exceeding it results in 429 responses for the rest of the month. For most applications, the quota is straightforward to stay within — but for high-growth SaaS or batch import scenarios, you need to track your usage. The TaxID API does not expose a quota status endpoint, so track usage in your application by counting validation calls in your database.

The most effective quota management strategy is aggressive caching combined with deduplication. Before making an API call, check your database for a recent validation of the same VAT number (within 24 hours). If one exists and was not service_unavailable, return the cached result without an API call. This reduces your API call volume significantly for applications where the same customers validate repeatedly (for example, every time they log in or update billing details).

typescriptlib/vat-dedup.ts
export async function validateWithDedup(
  vatNumber: string,
  customerId: string
) {
  const normalised = vatNumber.toUpperCase().replace(/\s/g, '');
  const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60_000);

  // Check database cache first
  const recent = await prisma.vatValidation.findFirst({
    where: {
      vatNumber: normalised,
      checkedAt: { gte: twentyFourHoursAgo },
      status: { not: 'service_unavailable' },
    },
    orderBy: { checkedAt: 'desc' },
  });

  if (recent) {
    return {
      isValid: recent.isValid,
      status: recent.status,
      companyName: recent.companyName,
      fromDbCache: true,
    };
  }

  // No recent result — call the API
  return validateAndStore(normalised, customerId, 'checkout');
}

Error Monitoring and Observability

Instrument your VAT validation code with logging and metrics from the start. The metrics you want: total validation calls per hour (to detect quota burn), service_unavailable rate per country (to detect VIES outages by member state), format_invalid rate (elevated rate may indicate a form input problem), and p95 response time (should be under 50ms for mostly-cached traffic). Send these to your existing observability stack — Datadog, Grafana, or a simple log aggregator.

For production incidents, the request_id in every TaxID API response is your trace identifier. Log it alongside your application's trace ID on every validation call. If a customer reports a billing issue that may be related to VAT validation, you can use the request_id to reconstruct exactly what the API returned at the time of the relevant checkout or invoice. This is also valuable for VAT audit purposes — you can prove to a tax authority what VIES said about a specific customer at a specific time.

typescriptlib/vat-instrumented.ts
import { metrics } from './observability'; // your metrics client
import { validateVat } from './vat';

export async function validateVatInstrumented(vatNumber: string) {
  const start = performance.now();
  const country = vatNumber.slice(0, 2).toUpperCase();

  try {
    const result = await validateVat(vatNumber);
    const duration = performance.now() - start;

    metrics.histogram('vat.validation.duration_ms', duration, { country });
    metrics.increment('vat.validation.calls', { status: result.status, country });

    if (result.status === 'service_unavailable') {
      metrics.increment('vat.vies.downtime', { country });
    }

    console.info('[vat-validation]', {
      vatNumber: vatNumber.slice(0, 4) + '****', // mask for logs
      country,
      status: result.status,
      cached: result.cached,
      duration_ms: Math.round(duration),
      request_id: result.requestId,
    });

    return result;
  } catch (err) {
    metrics.increment('vat.validation.error', { country });
    throw err;
  }
}

Validating VAT Numbers in Server Actions (Next.js 14)

Next.js 14 Server Actions let you call server-side code directly from client components without writing an API route. For VAT validation in a Next.js application, a Server Action is the cleanest approach — the validation logic runs on the server, the API key never reaches the client, and the result is returned directly to the form component.

typescriptapp/actions/validate-vat.ts
'use server';

import { vatValidator } from '@/lib/VatValidator';

export async function validateVatAction(
  vatNumber: string
): Promise<{ valid: boolean; status: string; companyName: string | null }> {
  const result = await vatValidator.validate(vatNumber);
  return {
    valid: result.isValid,
    status: result.status,
    companyName: result.companyName,
  };
}

// Usage in a Client Component:
// const result = await validateVatAction(vatNumber);

Common TypeScript Pitfalls

The most common TypeScript mistake when working with VAT validation is narrowing on the valid boolean instead of the status string. TypeScript does not know that status: 'service_unavailable' implies valid: false — it sees them as two independent fields. If you write if (!result.isValid) { blockUser() }, you will incorrectly block users during VIES downtime. The correct pattern is always to check status first and derive the action from it, not from the boolean alone.

A second common mistake is typing the VAT number input as string and forgetting that users can submit empty strings, null from a JSON body, or undefined from a missing form field. Always validate and sanitise the input before passing it to the validator. The TaxID API returns format_invalid for empty or null inputs, but catching them in your application code before making the API call is better practice — it saves an API call and gives you more control over the error message shown to the user.

For TypeScript projects using strict null checks (which you should be using), the company_name and address fields in the API response are typed as string | null. Some EU member states do not return address or company name through VIES. Always handle the null case in your code — do not assume these fields will be populated. For invoice generation specifically, you need a fallback when company_name is null: use the name the customer provided at signup rather than leaving the invoice field blank.

Performance Considerations for High-Traffic Applications

For high-traffic applications where the same VAT numbers are validated many times (common in marketplaces where seller validation is checked on every API request), the in-memory cache in VatValidator can grow unboundedly if you instantiate a new instance per request rather than using a singleton. Always export a single VatValidator instance as a module-level singleton, as shown in the code above. In serverless environments (Vercel Edge Functions, AWS Lambda), module-level singletons persist across warm invocations of the same container but are lost on cold starts — size your cache expectations accordingly.

For serverless environments with frequent cold starts, the Redis caching variant (VatValidatorRedis) is more appropriate than the in-memory Map. Upstash Redis — which the TaxID infrastructure itself uses — has a Node.js HTTP client that works well in serverless environments without maintaining a persistent connection. The cache TTL should be set to 23 hours (slightly less than TaxID's own 24-hour cache) to ensure your application cache and TaxID's Redis cache refresh in sync.

If you are building an application where VAT validation is on the critical path for every request (for example, validating the authenticated user's VAT status on every API call), consider storing the validation result directly in your session token or JWT rather than re-querying on every request. A JWT claim like vatStatus: 'b2b' with a validatedAt timestamp lets your middleware skip the validation API call entirely for already-verified users, while flagging users whose validation has expired (last checked more than 24 hours ago) for re-validation on their next request.

Handling Batch Validation in Node.js

Some workflows require validating many VAT numbers in bulk — importing a CSV of suppliers, migrating a customer database from another system, or running a quarterly re-validation sweep on all active B2B customers. Batch validation in Node.js requires a concurrency-limited queue to avoid overwhelming your API quota in a burst and to respect any per-second rate limits.

The key constraint for batch validation is your monthly quota. If you have 5,000 customer records to validate on the Starter plan (10,000 req/month), you are using half your monthly quota in one run. Plan batch jobs to run during quiet periods and track the number of API calls made. Use database deduplication (as shown in the vat-dedup.ts example above) to avoid re-validating numbers that were already checked recently — this can reduce a batch of 5,000 records to a few hundred actual API calls if most customers were validated in the last 24 hours.

typescriptlib/batch-validate.ts
import pLimit from 'p-limit'; // npm install p-limit
import { validateAndStore } from './vat-with-db';

interface BatchResult {
  vatNumber: string;
  status: string;
  companyName: string | null;
}

export async function batchValidate(
  records: Array<{ customerId: string; vatNumber: string }>,
  concurrency = 5
): Promise<BatchResult[]> {
  const limit = pLimit(concurrency); // max 5 concurrent API calls

  const results = await Promise.allSettled(
    records.map(({ customerId, vatNumber }) =>
      limit(async () => {
        const result = await validateAndStore(vatNumber, customerId, 'revalidation');
        return {
          vatNumber,
          status: result.status,
          companyName: result.companyName,
        };
      })
    )
  );

  return results
    .filter((r): r is PromiseFulfilledResult<BatchResult> => r.status === 'fulfilled')
    .map(r => r.value);
}

The p-limit library provides concurrency control for Promise arrays — it runs up to N promises concurrently and queues the rest. With concurrency set to 5, you send 5 validation requests simultaneously, wait for any to complete, then start the next queued request. This provides good throughput without overwhelming the API or your database with simultaneous writes. For very large batches (10,000+ records), add a delay between batches and log progress to avoid silent failures going unnoticed.

Putting It All Together: Production Checklist

Before shipping a Node.js EU VAT validation integration to production, verify these items are in place. Each point corresponds to a section in this tutorial or a linked guide.

Node.js is the most common runtime for new backend services in 2026, and the patterns in this tutorial translate directly to Deno, Bun, and any other JavaScript runtime with a fetch-compatible HTTP client. The TypeScript types, the VatValidator class pattern, the Express middleware, the Vitest test suite, and the Redis caching approach all work with minimal changes across runtimes. If you are using a different language, see the Python VAT validation guide and PHP Laravel VAT guide for equivalent implementations in those ecosystems. The API endpoints, authentication scheme, and response format are identical regardless of which language you use to call them.

The patterns in this tutorial — typed responses, status-based branching, in-memory caching, Redis for distributed deployments, Express middleware, Next.js Server Actions, Vitest mocks, and batch processing with concurrency limits — represent a production-grade Node.js VAT validation integration. Each pattern addresses a specific failure mode observed in real production systems: the typed responses prevent silent boolean mishandling, the status branch covers the VIES downtime case that breaks unchecked integrations, the cache reduces API call volume and covers checkout flows during downtime windows, and the test suite ensures all four status codes are handled correctly before the code ships. Start with the basic validateVat function, add the VatValidator singleton for caching, add the database storage for compliance, and add the tests before you ship. The more complex patterns (circuit breaker, batch processing, Redis cache) can be added incrementally as your scale and reliability requirements grow.

Start validating EU VAT numbers

Free plan — 100 validations/month. No credit card required.

Related articles