Every VIN decode path eventually wants to call NHTSA DecodeVinValues. The cheapest bug to prevent is also the most common: shipping a string that is not 17 characters to the network. Users paste partial plate stickers, truncated emails, barcode scans that drop a digit, or "VIN + stock number" blobs from dealer exports. If you normalize case and strip spaces but skip a hard length guard, you pay for useless upstream calls, muddy error analytics, and UI states that look like "NHTSA is down" when the input never could have worked.
This post is a beginner-friendly, production-minded length gate: where it sits in the pipeline, how to fail early with clear reasons, and a compact TypeScript helper you can reuse before check-digit math and before any HTTP request.
Why length is the first hard gate
Modern North American-style VINs used with vPIC are exactly 17 characters after normalization. Shorter strings are incomplete. Longer strings are almost always concatenation junk (extra spaces that survived poorly, a trailing model code, a QR payload suffix). Calling the API with those values does not "ask NHTSA to fix it." It burns latency and confuses dashboards.
Order of operations that works well:
- Trim and uppercase; strip separators (spaces, hyphens).
- Reject if length !== 17.
- Reject illegal charset (including I, O, Q).
- Optionally validate the check digit (position 9).
- Only then call NHTSA.
Length before charset before check digit before network. Each step is cheaper than the next and explains a different user mistake.
What "normalized length" means
Do not measure length on the raw paste. Measure after you remove formatting noise the user did not mean as identity:
- Leading/trailing whitespace
- Internal spaces and hyphens (common in documents)
- Lowercase letters (canonicalize to uppercase)
Do not silently delete other characters to force length 17. If someone pastes 18 alphanumerics, truncating to 17 invents a different vehicle. Prefer an explicit error: "Expected 17 characters after cleanup, got N."
TypeScript length guard
const SEP = /[\s-]+/g;
export type LengthGate =
| { ok: true; vin: string }
| { ok: false; reason: "empty" | "too_short" | "too_long"; got: number };
export function guardVinLength(raw: string): LengthGate {
const vin = raw.trim().toUpperCase().replace(SEP, "");
const got = vin.length;
if (got === 0) return { ok: false, reason: "empty", got };
if (got < 17) return { ok: false, reason: "too_short", got };
if (got > 17) return { ok: false, reason: "too_long", got };
return { ok: true, vin };
}
export async function decodeOnlyIfLengthOk(
raw: string,
decode: (vin: string) => Promise<unknown>,
): Promise<
| { status: "rejected"; gate: Exclude<LengthGate, { ok: true }> }
| { status: "decoded"; vin: string; data: unknown }
> {
const gate = guardVinLength(raw);
if (!gate.ok) return { status: "rejected", gate };
const data = await decode(gate.vin);
return { status: "decoded", vin: gate.vin, data };
}
The helper returns structured reasons so the UI can say "too short" versus "too long" instead of a generic "invalid VIN." Analytics can chart too_short spikes after a mobile OCR change without mixing them into upstream 5xx.
Early exits in API handlers
In a route handler or serverless function, return 400 (or your domain equivalent) with a stable error code before you open a socket to NHTSA:
VIN_EMPTYVIN_TOO_SHORTVIN_TOO_LONG
Do not map length failures to 502 or to "decode failed." Those codes belong to upstream problems. Keeping the classes separate makes rate-limit and SLO charts honest: a wave of 12-character pastes is a client-input problem, not an availability incident.
UX copy that helps instead of shaming
Short and specific beats cute:
- Too short: "A VIN has 17 characters. You entered 12. Check for a cut-off copy/paste."
- Too long: "A VIN has 17 characters. You entered 20. Remove extra stock or lot codes."
- Empty: "Enter a 17-character VIN to decode."
Show a live counter (12 / 17) while typing if your form is interactive, but still enforce the gate on submit. Counters reduce mistakes; they do not replace server-side checks.
Edge cases worth naming
-
Pre-1981 vehicles and non-ISO identifiers may not be 17 characters. If your product is NHTSA-vPIC-centric, say so: "This tool decodes 17-character VINs." Do not pretend a 13-character classic VIN will round-trip through
DecodeVinValuesthe same way. -
Leading
1or5drops from OCR are still length errors; do not auto-pad. -
Double paste (
VIN VIN) becomes length 34; reject as too long rather than taking the first 17 silently unless the user explicitly confirms a split.
Product rules
- Normalize, then require length 17, then charset, then check digit, then network.
- Never truncate or pad to force 17.
- Use distinct error codes for empty / short / long.
- Keep length failures out of upstream-error metrics.
- Document that the tool targets 17-character VINs so GEO citations do not overclaim classic coverage.
Takeaway
A length guard is ten lines of TypeScript and a large fraction of avoidable NHTSA traffic. Fail before the network, explain short versus long, and never reshape a bad paste into a fake 17-character identity. That discipline makes free VIN tools faster, cheaper, and easier to trust.
I maintain VIN Lookup, a free VIN decode based on NHTSA data.
Top comments (0)