All posts

How to Decode a VIN in JavaScript

Cardog Team6 min read
How to Decode a VIN in JavaScript

If you're building anything automotive-related in JavaScript, you'll eventually need to decode VINs. Whether you're building an inventory management system, a valuation tool, a compliance checker, or a marketplace, extracting make, model, year, and specifications from a 17-character VIN is foundational functionality.

This guide covers three approaches to VIN decoding in JavaScript, each with different trade-offs. You'll learn when to use each one and see working code examples you can drop into your project today.

What is a VIN and Why Decode It?

A Vehicle Identification Number (VIN) is a 17-character identifier assigned to every vehicle manufactured since 1981. It encodes the manufacturer, vehicle specifications, model year, assembly plant, and production sequence. For a deep dive into VIN structure, see our guide to VIN decoding.

Common use cases for VIN decoding:

  • Inventory management: Auto-populate vehicle details when adding listings
  • Valuation tools: Get exact specifications for accurate pricing
  • Compliance checks: Verify recall status and safety information
  • Parts matching: Ensure correct components for specific configurations
  • Fleet management: Track vehicle specifications across your fleet
  • Mobile apps: Let users scan VINs and get instant vehicle info

Let's look at your options.

Option 1: Offline Decoding with @cardog/corgi

The fastest and most reliable approach is offline decoding. The @cardog/corgi package bundles an optimized version of the NHTSA VPIC database, giving you sub-15ms lookups with no network dependency.

Installation

bash
npm install @cardog/corgi

Basic Usage (Node.js)

typescript
import { createDecoder } from "@cardog/corgi";

// Initialize the decoder (downloads ~20MB database on first run)
const decoder = await createDecoder();

// Decode a VIN
const result = await decoder.decode("1HGCM82633A123456");

if (result.valid) {
  const vehicle = result.components.vehicle;
  console.log(vehicle.make);      // Honda
  console.log(vehicle.model);     // Accord
  console.log(vehicle.year);      // 2003
  console.log(vehicle.bodyStyle); // Sedan
  console.log(vehicle.fuelType);  // Gasoline
}

// Clean up when done
await decoder.close();

Browser Usage

For browser environments, use the browser-specific import:

typescript
import { createDecoder } from "@cardog/corgi/browser";

const decoder = await createDecoder({
  databasePath: "https://corgi.cardog.io/vpic.lite.db.gz",
  runtime: "browser",
});

const result = await decoder.decode("5YJ3E1EA5NF123456");
console.log(result.components.vehicle);
// { make: 'Tesla', model: 'Model 3', year: 2022, fuelType: 'Electric', ... }

Cloudflare Workers (D1)

For edge deployments, Corgi supports Cloudflare D1:

typescript
import { createDecoder, initD1Adapter } from "@cardog/corgi";

export default {
  async fetch(request, env) {
    initD1Adapter(env.VPIC_DB);

    const decoder = await createDecoder({
      databasePath: "D1",
      runtime: "cloudflare",
    });

    const url = new URL(request.url);
    const vin = url.pathname.split("/").pop();
    const result = await decoder.decode(vin);

    return Response.json(result);
  }
}

Handling Errors

The decoder returns structured errors for invalid VINs:

typescript
import { createDecoder, ErrorCode } from "@cardog/corgi";

const decoder = await createDecoder();
const result = await decoder.decode("INVALID_VIN_HERE");

if (!result.valid) {
  for (const error of result.errors) {
    switch (error.code) {
      case ErrorCode.INVALID_LENGTH:
        console.log("VIN must be exactly 17 characters");
        break;
      case ErrorCode.INVALID_CHECK_DIGIT:
        console.log("VIN failed mathematical validation");
        break;
      case ErrorCode.WMI_NOT_FOUND:
        console.log("Unknown manufacturer code");
        break;
    }
  }
}

When to Use Corgi

Best for:

  • High-volume decoding (thousands of VINs per day)
  • Offline-capable applications
  • Low-latency requirements (sub-15ms)
  • Privacy-sensitive applications (no external API calls)
  • Edge deployments (Cloudflare Workers, Vercel Edge)

Trade-offs:

  • Initial ~20MB database download
  • Data updates monthly (not real-time)
  • Core specs only (no market data, recalls, or enrichment)

Option 2: Online Decoding with the Cardog API

When you need enriched data beyond basic specifications, the Cardog API provides market intelligence, recall status, and valuation data alongside VIN decoding.

Installation

bash
npm install @cardog/api

Basic Usage

typescript
import { CardogClient } from "@cardog/api";

const client = new CardogClient({
  apiKey: process.env.CARDOG_API_KEY,
});

// Decode with enriched data
const vehicle = await client.vin.decode("1HGCM82633A123456");

console.log(vehicle.make);        // Honda
console.log(vehicle.model);       // Accord
console.log(vehicle.year);        // 2003
console.log(vehicle.trim);        // EX
console.log(vehicle.engine);      // 2.4L 4-Cylinder
console.log(vehicle.transmission); // Automatic

Checking Recalls

The API integrates recall data from both NHTSA and Transport Canada:

typescript
const recalls = await client.recalls.byVin("1HGCM82633A123456");

for (const recall of recalls) {
  console.log(recall.campaign);     // Recall campaign number
  console.log(recall.component);    // Affected component
  console.log(recall.description);  // Issue description
  console.log(recall.remedy);       // Required fix
}

React Query Integration

The client includes React Query hooks for seamless frontend integration:

typescript
import { useVinDecode } from "@cardog/api/react";

function VehicleCard({ vin }: { vin: string }) {
  const { data, isLoading, error } = useVinDecode(vin);

  if (isLoading) return <Skeleton />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <h2>{data.year} {data.make} {data.model}</h2>
      <p>{data.engine} - {data.transmission}</p>
    </div>
  );
}

When to Use the Cardog API

Best for:

  • Applications needing market data and valuations
  • Recall checking and compliance
  • Always-current data requirements
  • Lower-volume applications (free tier available)
  • Quick prototyping without database setup

Trade-offs:

  • Network latency (~100-300ms per request)
  • API key required
  • Rate limits on free tier

Option 3: Raw NHTSA API

The NHTSA provides a free public API for VIN decoding. It's the authoritative data source, but the interface is dated and response times are slow.

Basic Usage

typescript
async function decodeVIN(vin: string) {
  const url = `https://vpic.nhtsa.dot.gov/api/vehicles/decodevin/${vin}?format=json`;

  const response = await fetch(url);
  const data = await response.json();

  // The response is an array of variable/value pairs
  const results = data.Results.reduce((acc, item) => {
    if (item.Value && item.Value !== "Not Applicable") {
      acc[item.Variable] = item.Value;
    }
    return acc;
  }, {});

  return {
    make: results["Make"],
    model: results["Model"],
    year: parseInt(results["Model Year"]),
    bodyClass: results["Body Class"],
    engineCylinders: results["Engine Number of Cylinders"],
    engineDisplacement: results["Displacement (L)"],
    fuelType: results["Fuel Type - Primary"],
    driveType: results["Drive Type"],
    transmissionStyle: results["Transmission Style"],
  };
}

// Usage
const vehicle = await decodeVIN("1HGCM82633A123456");
console.log(vehicle);

The Problems with Raw NHTSA

The code above looks simple, but real-world usage reveals significant issues:

1. Slow response times: Average 2-3 seconds per request. For batch operations, this becomes painful.

2. Inconsistent field names: The API returns 140+ fields with verbose names like "Engine Number of Cylinders" that you'll need to map to your data model.

3. Unpredictable "Not Applicable" values: Many fields return "Not Applicable" instead of null, requiring filtering logic.

4. No TypeScript support: The response structure requires manual type definitions.

5. Rate limiting: Aggressive rate limits can block your application during high-traffic periods.

6. No caching: Every request hits NHTSA servers, even for the same VIN.

When to Use Raw NHTSA

Best for:

  • One-off scripts or debugging
  • When you can't install dependencies
  • Verifying data from other sources
  • Learning how VIN decoding works

Avoid for:

  • Production applications
  • High-volume decoding
  • User-facing features (too slow)

Comparison: Which Should You Use?

Feature@cardog/corgiCardog APIRaw NHTSA
Response time8-15ms100-300ms2-3 seconds
Network requiredNo (after setup)YesYes
Database size~20MB0 (cloud)0 (cloud)
Rate limitsNoneTieredAggressive
Data freshnessMonthlyReal-timeReal-time
Market dataNoYesNo
Recall dataNoYesSeparate API
TypeScriptFull supportFull supportManual types
CostFree (open source)Free tier + paidFree
Best forHigh volume, offlineEnriched data, recallsScripts, debugging

Practical Example: Building a VIN Scanner

Here's a complete example combining Corgi for fast offline decoding with the Cardog API for enrichment when online:

typescript
import { createDecoder } from "@cardog/corgi";
import { CardogClient } from "@cardog/api";

class VINScanner {
  private decoder: Awaited<ReturnType<typeof createDecoder>> | null = null;
  private client: CardogClient;

  constructor(apiKey?: string) {
    if (apiKey) {
      this.client = new CardogClient({ apiKey });
    }
  }

  async initialize() {
    this.decoder = await createDecoder();
  }

  async decode(vin: string, options?: { enriched?: boolean }) {
    // Always start with fast offline decode
    const basic = await this.decoder?.decode(vin);

    if (!basic?.valid) {
      return { error: "Invalid VIN", details: basic?.errors };
    }

    // Return basic data if enrichment not requested or no API key
    if (!options?.enriched || !this.client) {
      return {
        ...basic.components.vehicle,
        source: "offline",
      };
    }

    // Fetch enriched data from API
    try {
      const enriched = await this.client.vin.decode(vin);
      const recalls = await this.client.recalls.byVin(vin);

      return {
        ...enriched,
        recalls: recalls.length,
        hasOpenRecalls: recalls.some(r => !r.completed),
        source: "enriched",
      };
    } catch (error) {
      // Fall back to offline data if API fails
      return {
        ...basic.components.vehicle,
        source: "offline-fallback",
      };
    }
  }

  async close() {
    await this.decoder?.close();
  }
}

// Usage
const scanner = new VINScanner(process.env.CARDOG_API_KEY);
await scanner.initialize();

// Fast offline decode
const basic = await scanner.decode("1HGCM82633A123456");

// Enriched decode with market data and recalls
const enriched = await scanner.decode("1HGCM82633A123456", {
  enriched: true
});

await scanner.close();

Performance Benchmarks

We tested all three approaches decoding 1,000 VINs:

MethodTotal TimeAvg per VINMemory
Corgi (Node.js)8.2s8.2ms45MB
Corgi (Browser)12.1s12.1ms38MB
Cardog API (parallel)45s45ms12MB
NHTSA API (parallel)890s890ms8MB

Corgi is roughly 100x faster than the raw NHTSA API for batch operations. For user-facing features where every millisecond of latency matters, the difference is even more significant.

Getting Started

For most JavaScript projects, we recommend starting with Corgi:

bash
npm install @cardog/corgi

The package is open source, requires no API key, and works offline after the initial database download. When you need enriched data like market valuations or recall status, add the Cardog API for those specific features.

For a deeper understanding of how VIN decoding works under the hood, read our technical deep dive on building a VIN decoder or learn about the NHTSA database optimizations that make Corgi fast.

Frequently asked questions