All projects
04 / 07 Open source Case study

vinplain

A TypeScript client for US vehicle records: VIN decode, recalls, complaints, crash ratings, and fuel economy.

Role
Creator, sole author
Status
Open source · live on npm
Period
2026
vinplain · package surface and a decoded VIN record.
Project facts
Public data sources
3
Runtime dependencies
0
Deterministic tests
34
Package manager
npm
Scope
TypeScript client, CLI, docs, Expo OCR companion
Focus
Public data · Failure semantics · Developer tooling · Release engineering
Stack
TypeScript · Node · vPIC · NHTSA · EPA · npm · GitHub Actions · Expo

The public record has no client

A VIN is a 17-character public identifier. The data behind it is spread across three federal services: vPIC decodes the vehicle, NHTSA publishes recalls, complaints, investigations and crash ratings, and the Department of Energy publishes fuel economy. Each has its own response shape and its own way of failing.

Most packages stop at year, make and model. The paid APIs go further, but they add a vendor account and a per-lookup fee. I wanted the public record itself, in one typed call.

What it does

getVehicleSummary({ vin }) decodes the VIN, then runs the record lookups in parallel. It returns the specs, recall campaigns, complaint totals, NCAP ratings, fuel economy, and the investigation flag, with a status for each section.

const record = await getVehicleSummary({ vin: '1HGCM82633A004352' });

record.specs;           // 2003 Honda Accord EX-V6
record.recalls.length;  // 24
record.status.recalls;  // 'ok'

The CLI returns the same record as text or JSON:

npx vinplain 1HGCM82633A004352

The decision that shaped it

The important part is not the happy path. It is what happens when a source is down.

null means the request failed. [] means the source answered and there is nothing on file. A failed recall check must not render as a clean record. The summary carries the same distinction in a per-section status, so a UI can say “couldn’t check” without pretending it checked.

That decision shaped the types, the tests, and the documentation. The deterministic suite mocks response shapes confirmed against the live services. Four opt-in tests call the real APIs. There are no runtime dependencies.

The extraction

vinplain started as the research layer inside VINly. The product code stays private; the reusable part came out as a separate MIT package.

The extraction excludes the listings, the record-pulse scoring, the peer comparisons, and the UI. What remains is the part that talks to public data: VIN validation and the ISO 3779 check digit, vPIC decode, NHTSA records, EPA economy, a cache seam, and configurable timeouts.

The API

The package is on npm as vinplain, with a CLI, an API reference, a failure model, and source notes. There is an iOS companion, expo-vin-ocr, that reads a VIN from a photo with Apple Vision and hands the text to the same validation layer. The photo stays on the phone.

The website is a one-page product record. It answers three questions: what it does, how to install it, and what it will not do.

What this work taught me

Public data is messy. The recall endpoint returns the same 400 for a valid car with no campaigns and for a model name it does not recognise. A client that treats both as “no recalls” is wrong in a way that matters. Most of the work was mapping the failure modes and putting them in the types.

The package is more useful because it does not try to be a history report, an offline decoder, or a hosted API. It reads the public record and stops.