Skip to Content

Server-Side Country Code Validation in Node.js

Client-side validation is for user experience. Server-side validation is for data integrity. Any country code that arrives at your API — from form submissions, webhook payloads, third-party integrations, or mobile apps — must be validated before it touches your database.

This guide covers Express middleware for country validation, API endpoint design, input normalization, and error handling patterns using @koshmoney/countries.

Why Server-Side Validation Matters

Country codes appear everywhere in backend systems: user profiles, shipping addresses, KYC records, payment processing, and compliance checks. Invalid codes cause cascading failures:

  • Database queries return wrong results
  • Payment processors reject transactions
  • Compliance checks produce false negatives
  • Reports and analytics become unreliable

A single validation layer at the API boundary prevents all of these problems.

Basic Validation Functions

Validate a Country Code

import { country } from '@koshmoney/countries'; // Validate any format (alpha-2, alpha-3, or numeric) country.isValid('US'); // true country.isValid('USA'); // true country.isValid(840); // true country.isValid('XX'); // false country.isValid(''); // false // Validate a specific format country.isAlpha2('US'); // true country.isAlpha2('USA'); // false (that's alpha-3) country.isAlpha3('USA'); // true country.isNumeric(840); // true // Detect the format country.detectFormat('US'); // 'alpha2' country.detectFormat('USA'); // 'alpha3' country.detectFormat('840'); // 'numeric' country.detectFormat('XX'); // null

Validate a Subdivision Code

import { subdivision } from '@koshmoney/countries'; // Validate full ISO 3166-2 code subdivision.isValidCode('US-CA'); // true subdivision.isValidCode('US-XX'); // false subdivision.isValidCode('XX-YY'); // false // Validate region code within a country subdivision.isValidRegion('US', 'CA'); // true subdivision.isValidRegion('US', 'XX'); // false // Validate by name subdivision.isValidName('US', 'California'); // true subdivision.isValidName('US', 'Atlantis'); // false

Validate Postal Codes

import { postalCode } from '@koshmoney/countries'; postalCode.isValid('US', '90210'); // true postalCode.isValid('US', 'ABCDE'); // false postalCode.isValid('GB', 'SW1A 1AA'); // true postalCode.isValid('CA', 'K1A 0B1'); // true

Express Middleware

Country Code Validation Middleware

Create a reusable middleware that validates country codes in request body, query parameters, or route params:

import { Request, Response, NextFunction } from 'express'; import { country } from '@koshmoney/countries'; interface ValidationOptions { field: string; source: 'body' | 'query' | 'params'; required?: boolean; format?: 'alpha2' | 'alpha3' | 'numeric' | 'any'; normalize?: boolean; // Convert to alpha-2 } function validateCountryCode(options: ValidationOptions) { const { field, source, required = true, format = 'any', normalize = true, } = options; return (req: Request, res: Response, next: NextFunction) => { const value = req[source]?.[field]; // Handle missing value if (!value && !required) return next(); if (!value) { return res.status(400).json({ error: 'VALIDATION_ERROR', message: `${field} is required`, field, }); } // Validate format let isValid = false; switch (format) { case 'alpha2': isValid = country.isAlpha2(value); break; case 'alpha3': isValid = country.isAlpha3(value); break; case 'numeric': isValid = country.isNumeric(Number(value)); break; default: isValid = country.isValid(value); } if (!isValid) { return res.status(400).json({ error: 'INVALID_COUNTRY_CODE', message: `Invalid country code: ${value}`, field, hint: format === 'alpha2' ? 'Expected a 2-letter ISO 3166-1 code (e.g., US, GB, DE)' : 'Expected a valid ISO 3166-1 code', }); } // Normalize to alpha-2 if requested if (normalize && format !== 'alpha2') { const detected = country.detectFormat(String(value)); if (detected === 'alpha3') { req[source][field] = country.alpha3ToAlpha2(value); } else if (detected === 'numeric') { req[source][field] = country.numericToAlpha2(Number(value)); } } next(); }; }

Using the Middleware

import express from 'express'; const app = express(); app.use(express.json()); // Validate country in request body app.post('/api/addresses', validateCountryCode({ field: 'countryCode', source: 'body', format: 'alpha2' }), (req, res) => { // req.body.countryCode is guaranteed to be a valid alpha-2 code res.json({ status: 'ok', country: req.body.countryCode }); } ); // Validate country in route params app.get('/api/countries/:code/subdivisions', validateCountryCode({ field: 'code', source: 'params', normalize: true }), (req, res) => { // req.params.code is normalized to alpha-2 const subs = subdivision.forCountry(req.params.code); res.json(subs); } ); // Optional country in query string app.get('/api/users', validateCountryCode({ field: 'country', source: 'query', required: false }), (req, res) => { // req.query.country is either valid or undefined res.json({ filter: req.query.country ?? 'all' }); } );

Full Address Validation Middleware

import { country, subdivision, postalCode } from '@koshmoney/countries'; interface AddressBody { countryCode: string; stateCode?: string; postalCodeValue?: string; city: string; line1: string; } function validateAddress(req: Request, res: Response, next: NextFunction) { const { countryCode, stateCode, postalCodeValue } = req.body as AddressBody; const errors: Array<{ field: string; message: string }> = []; // Country is always required if (!countryCode) { errors.push({ field: 'countryCode', message: 'Country code is required' }); } else if (!country.isAlpha2(countryCode)) { errors.push({ field: 'countryCode', message: `Invalid country code: ${countryCode}` }); } else { // Only validate dependent fields if country is valid if (stateCode && !subdivision.isValidRegion(countryCode, stateCode)) { errors.push({ field: 'stateCode', message: `Invalid state/province: ${stateCode} for ${countryCode}`, }); } if (!stateCode && subdivision.hasSubdivisions(countryCode)) { errors.push({ field: 'stateCode', message: 'State/province is required for this country', }); } if (postalCodeValue && !postalCode.isValid(countryCode, postalCodeValue)) { const format = postalCode.getFormat(countryCode); const name = postalCode.getName(countryCode) ?? 'Postal code'; errors.push({ field: 'postalCodeValue', message: `Invalid ${name}. Expected format: ${format}`, }); } if (!postalCodeValue && postalCode.hasPostalCode(countryCode)) { const name = postalCode.getName(countryCode) ?? 'Postal code'; errors.push({ field: 'postalCodeValue', message: `${name} is required` }); } } if (errors.length > 0) { return res.status(400).json({ error: 'VALIDATION_ERROR', errors }); } next(); }

API Endpoint Design

Country Lookup Endpoint

import { country, subdivision } from '@koshmoney/countries'; // GET /api/countries/:code app.get('/api/countries/:code', (req, res) => { const data = country.whereAlpha2(req.params.code.toUpperCase()); if (!data) { return res.status(404).json({ error: 'COUNTRY_NOT_FOUND', message: `No country found for code: ${req.params.code}`, }); } const subs = subdivision.forCountry(data.alpha2); res.json({ ...data, subdivisionCount: subs.length, hasSubdivisions: subs.length > 0, }); }); // GET /api/countries/:code/subdivisions app.get('/api/countries/:code/subdivisions', (req, res) => { const code = req.params.code.toUpperCase(); if (!country.isAlpha2(code)) { return res.status(400).json({ error: 'INVALID_COUNTRY_CODE', message: `Invalid country code: ${req.params.code}`, }); } const subs = subdivision.forCountry(code); res.json({ countryCode: code, count: subs.length, subdivisions: subs, }); });

Validation Endpoint

Expose a validation endpoint so clients can check addresses before submission:

// POST /api/validate/address app.post('/api/validate/address', (req, res) => { const { countryCode, stateCode, postalCodeValue } = req.body; const result: Record<string, boolean> = {}; result.countryValid = country.isAlpha2(countryCode); if (result.countryValid) { result.stateValid = !stateCode || subdivision.isValidRegion(countryCode, stateCode); result.postalCodeValid = !postalCodeValue || postalCode.isValid(countryCode, postalCodeValue); result.stateRequired = subdivision.hasSubdivisions(countryCode); result.postalCodeRequired = postalCode.hasPostalCode(countryCode); } result.valid = result.countryValid && result.stateValid !== false && result.postalCodeValid !== false; res.json(result); });

Input Sanitization

Country codes arrive in many formats. Normalize them before validation:

function normalizeCountryInput(input: unknown): string | null { if (typeof input !== 'string') return null; // Trim whitespace and convert to uppercase const cleaned = input.trim().toUpperCase(); // Reject empty strings if (cleaned.length === 0) return null; // Try alpha-2 first (most common) if (country.isAlpha2(cleaned)) return cleaned; // Try alpha-3 and convert if (country.isAlpha3(cleaned)) return country.alpha3ToAlpha2(cleaned); // Try numeric const num = Number(cleaned); if (!isNaN(num) && country.isNumeric(num)) return country.numericToAlpha2(num); // Try by name const byName = country.whereName(cleaned); if (byName) return byName.alpha2; return null; } normalizeCountryInput('us'); // 'US' normalizeCountryInput('USA'); // 'US' normalizeCountryInput('840'); // 'US' normalizeCountryInput('United States'); // 'US' normalizeCountryInput('Narnia'); // null normalizeCountryInput(undefined); // null

Error Response Patterns

Consistent Error Format

interface ApiError { error: string; message: string; field?: string; hint?: string; } function countryError(value: string, field: string): ApiError { return { error: 'INVALID_COUNTRY_CODE', message: `'${value}' is not a valid ISO 3166-1 country code`, field, hint: 'Use a 2-letter code like US, GB, or DE. See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2', }; } function subdivisionError(countryCode: string, stateCode: string): ApiError { const subs = subdivision.forCountry(countryCode); const validCodes = subs.slice(0, 5).map(s => s.regionCode).join(', '); return { error: 'INVALID_SUBDIVISION_CODE', message: `'${stateCode}' is not a valid subdivision of ${countryCode}`, field: 'stateCode', hint: `Valid codes include: ${validCodes}${subs.length > 5 ? ', ...' : ''}`, }; }

NestJS Integration

If you are using NestJS instead of Express, use a custom validation pipe:

import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'; import { country } from '@koshmoney/countries'; @Injectable() export class CountryCodePipe implements PipeTransform { transform(value: string): string { const normalized = value?.trim().toUpperCase(); if (!normalized || !country.isAlpha2(normalized)) { throw new BadRequestException(`Invalid country code: ${value}`); } return normalized; } } // Usage in controller @Get(':countryCode/subdivisions') getSubdivisions(@Param('countryCode', CountryCodePipe) countryCode: string) { return subdivision.forCountry(countryCode); }

DTO Validation with class-validator

import { IsString, Validate } from 'class-validator'; import { ValidatorConstraint, ValidatorConstraintInterface } from 'class-validator'; import { country } from '@koshmoney/countries'; @ValidatorConstraint({ name: 'isCountryCode', async: false }) class IsCountryCodeConstraint implements ValidatorConstraintInterface { validate(value: string) { return country.isAlpha2(value?.toUpperCase()); } defaultMessage() { return 'Must be a valid ISO 3166-1 alpha-2 country code'; } } class CreateAddressDto { @IsString() @Validate(IsCountryCodeConstraint) countryCode: string; }

Testing Your Validation

import { describe, it, expect } from 'vitest'; describe('Country validation middleware', () => { it('accepts valid alpha-2 codes', () => { expect(normalizeCountryInput('US')).toBe('US'); expect(normalizeCountryInput('gb')).toBe('GB'); expect(normalizeCountryInput('DE')).toBe('DE'); }); it('normalizes alpha-3 to alpha-2', () => { expect(normalizeCountryInput('USA')).toBe('US'); expect(normalizeCountryInput('GBR')).toBe('GB'); }); it('rejects invalid codes', () => { expect(normalizeCountryInput('XX')).toBeNull(); expect(normalizeCountryInput('')).toBeNull(); expect(normalizeCountryInput(undefined)).toBeNull(); }); it('handles numeric codes', () => { expect(normalizeCountryInput('840')).toBe('US'); }); });

Summary

  • Always validate country codes server-side, even if the client already validated them
  • Normalize input to alpha-2 format early in the request pipeline
  • Use middleware to keep validation logic out of route handlers
  • Return helpful error messages with the expected format and examples
  • Combine country, subdivision, and postal code validation for complete address checking