Best PracticesREST API

Input Validation Best Practices for REST APIs

Input validation is the first line of defence for any API. Done well, it prevents bad data from reaching your business logic, improves error messages for API consumers, and stops entire categories of security vulnerabilities before they start.

1. Why input validation matters

Every piece of data that enters your API is an assumption — a claim made by an external system or user. Validation is how you verify that claim before acting on it.

🔒
Security
Prevents injection attacks, path traversal, type confusion, and oversize payloads from reaching business logic.
📊
Data quality
Catches typos, format errors, and semantically invalid values (like structurally valid but unregistered tax IDs) at the boundary.
🛠️
Developer experience
Returns precise, actionable error messages instead of cryptic 500 errors from deep inside your stack.
⚠️Never trust client-side validation alone. A browser form might validate an IBAN, but an API consumer using curl or a malicious actor bypasses it entirely. All validation that affects data persistence or business logic must happen on the server.

2. Where to validate: client, server, or both?

LayerPurposeTrust level
Client (browser / mobile)Immediate feedback, reduce round-trips for obvious errorsZero trust — never authoritative
API gateway / middlewareSchema validation, rate limiting, auth token checksPartial — can be bypassed if misconfigured
Application layerBusiness rule validation, semantic checks, cross-field consistencyFull trust — this is the authoritative gate
Database layerNOT NULL, UNIQUE, FK constraints as a final safety netFull trust — but errors here are too late

The answer is both, with the server being authoritative. Client-side validation improves UX; server-side validation ensures correctness.


3. Two layers: format vs semantic validation

Most validation guides focus on format validation, but production APIs need both layers:

Format validation

Is the shape of the data correct?

  • ✓ Required fields present
  • ✓ Correct data type (string, number, boolean)
  • ✓ Length constraints
  • ✓ Regex pattern match
  • ✓ Enum values
Tools: Zod, Joi, Pydantic, JSON Schema

Semantic validation

Does the value actually mean something valid?

  • ✓ Checksum verification (Luhn, MOD-97)
  • ✓ Registry lookup (VAT, IBAN bank, ABN)
  • ✓ Cross-field consistency (IBAN country ↔ BIC country)
  • ✓ Domain-specific rules (SEPA eligibility, GSTIN state)
  • ✓ Real-world existence (ORCID registered, DOI resolved)
Tools: IsValid API
ℹ️Example: The IBAN GB29NWBK60161331926819 passes regex validation (correct length, correct country prefix). But is it real? Does the bank code match a real institution? Is it SEPA-eligible? These are semantic questions — format validation cannot answer them.

4. Identifiers that need semantic validation

These common identifier types all have internal checksums or registry backing that a simple regex cannot verify:

IdentifierCommon use caseWhat regex misses
IBANBank transfers, fintech onboardingMOD-97 checksum, SEPA eligibility, bank lookup
VAT / GSTB2B invoicing, tax complianceCountry-specific checksum algorithms, VIES registry
Credit cardPayments, PCI-DSSLuhn checksum, network detection, card type
GSTINIndia GST complianceState code validity, mod-36 check char, embedded PAN
ABNAustralian supplier onboardingMOD-89 checksum, ABR registry, GST status
BTC/ETH addressCrypto withdrawalsBase58Check, Bech32, EIP-55 checksum
IMEIDevice registration, IoTLuhn checksum, TAC validity
EORIEU customs / tradeCountry prefix rules, EC registry lookup
DOIAcademic publishingPrefix/suffix structure, CrossRef registration
LEIFinancial complianceMOD-97 checksum, GLEIF entity status

5. Parallel validation for performance

The most common mistake in API validation is running checks sequentially when they are independent. If a request contains both an IBAN and an email address, there is no reason to validate them one after the other.

// ❌ Sequential — total latency = sum of all calls (~300ms)
const ibanResult = await iv.iban(body.iban)
const emailResult = await iv.email(body.email)
const vatResult = await iv.vat(body.vat)

// ✅ Parallel — total latency = slowest single call (~100ms)
const [ibanResult, emailResult, vatResult] = await Promise.all([
  iv.iban(body.iban),
  iv.email(body.email),
  iv.vat(body.vat),
])

Use Promise.allSettled (Node.js) or asyncio.gather(return_exceptions=True) (Python) when you want to collect all validation errors in a single pass rather than failing on the first one.

// Collect all errors in one round-trip
const results = await Promise.allSettled([
  iv.iban(body.iban),
  iv.email(body.email),
  iv.vat(body.vat),
])

const errors: Record<string, string> = {}
const fields = ['iban', 'email', 'vat']
results.forEach((r, i) => {
  if (r.status === 'fulfilled' && !r.value.valid) {
    errors[fields[i]] = 'Invalid value'
  }
})

if (Object.keys(errors).length > 0) {
  return res.status(422).json({ errors })
}

6. Error response format (RFC 9457)

Use HTTP 422 Unprocessable Content for validation failures — not 400 (which is for malformed syntax) and not 500 (which signals your fault, not theirs). RFC 9457 (Problem Details) provides a standard structure:

{
  "type": "https://isvalid.dev/errors/validation-failed",
  "title": "Validation Failed",
  "status": 422,
  "detail": "One or more fields failed validation",
  "errors": {
    "iban": "Invalid IBAN — MOD-97 checksum failed",
    "vat": "VAT number not found in VIES registry",
    "email": "Domain does not have MX records"
  }
}
Status codeWhen to use
400 Bad RequestMalformed JSON, wrong Content-Type, missing required headers
422 Unprocessable ContentWell-formed request but invalid field values — this is the right code for validation failures
409 ConflictValid data but conflicts with existing state (duplicate email, already registered)
500 Internal Server ErrorYour bug — never return this for a validation failure

7. Validation middleware patterns

Centralise validation in middleware so route handlers only run with clean, verified data.

Express.js — validation middleware

import { createClient } from '@isvalid-dev/sdk'
const iv = createClient({ apiKey: process.env.ISVALID_API_KEY! })

export async function validatePaymentBody(req, res, next) {
  const { iban, bic } = req.body
  const errors: Record<string, string> = {}

  const [ibanR, bicR] = await Promise.all([
    iv.iban(iban).catch(() => ({ valid: false })),
    iv.bic(bic).catch(() => ({ valid: false })),
  ])

  if (!ibanR.valid) errors.iban = 'Invalid IBAN'
  if (!bicR.valid) errors.bic = 'Invalid BIC'

  if (Object.keys(errors).length > 0) {
    return res.status(422).json({ status: 422, title: 'Validation Failed', errors })
  }
  next()
}

FastAPI — dependency injection

from fastapi import Depends, HTTPException
from isvalid_sdk import IsValidConfig, create_client
import asyncio

iv = create_client(IsValidConfig(api_key="YOUR_API_KEY"))

async def validate_payment(iban: str, bic: str):
    iban_r, bic_r = await asyncio.gather(iv.iban(iban), iv.bic(bic))
    errors = {}
    if not iban_r.valid: errors["iban"] = "Invalid IBAN"
    if not bic_r.valid:  errors["bic"]  = "Invalid BIC"
    if errors:
        raise HTTPException(status_code=422, detail={"errors": errors})
    return {"iban": iban_r, "bic": bic_r}

@app.post("/payments")
async def create_payment(validated = Depends(validate_payment)):
    # validated is clean — route handler never sees invalid data
    ...

8. Checklist

Always validate on the server — never trust the client alone
Run format validation (schema) before semantic validation
Use Promise.all / asyncio.gather for parallel checks
Return 422 (not 400 or 500) for validation failures
Include field-level error messages in the response
Use RFC 9457 Problem Details structure
Centralise validation in middleware, not route handlers
Log validation failures for anomaly detection
Validate checksums for financial identifiers (IBAN, LEI, CUSIP)
Check registry status for tax IDs (VAT, ABN, GSTIN)

Related guides

Start validating with one API

50+ identifier types. IBAN, VAT, GSTIN, ABN, credit card, IMEI, and more — one SDK, one key.

Get your API key →