πŸš— vehicles – Car makes & models for your Rails app

Gem Version Build Status

[!TIP] πŸš€ Ship your next Rails app 10x faster! I've built RailsFast, a production-ready Rails boilerplate template that comes with everything you need to launch a software business in days, not weeks. Go check it out!

vehicles gives your Rails app a clean, curated list of car makes and models β€” ready for dropdowns, search, and validation. No API keys, no database table, no migration β€” it works fully offline the second you bundle install, because the data ships inside the gem. (Optionally, it can refresh the data without a gem upgrade.)

✨ Perfect for marketplaces, carpooling & rideshare apps, fleet tools, parking & EV-charging apps, insurance and booking forms β€” anywhere a user has to pick their vehicle.

Check out my other πŸ’Ž Ruby gems: usage_credits Β· profitable Β· api_keys Β· nondisposable Β· trackdown

πŸ‘¨β€πŸ’» Example

Vehicles.makes
# => ["Alfa Romeo", "Audi", "BMW", "BYD", "CitroΓ«n", "Cupra", "Dacia", "Fiat", ...]

Vehicles.models("VW")              # alias-aware β€” "VW" just works
# => ["Golf", "Polo", "Tiguan", "Passat", "T-Roc", "ID.3", "ID.4", "T-Cross", ...]

car = Vehicles.find("vw golf")     # one fuzzy lookup, make + model in a single string
car.make                           # => "Volkswagen"
car.name                           # => "Golf"
car.full_name                      # => "Volkswagen Golf"
car.kind                           # => :car
car.body_type                      # => :hatchback
car.hatchback?                     # => true

…and the whole reason this gem exists β€” a make β†’ model picker in two lines:

<%= form.select :make,  Vehicles.make_options,             prompt: "Make" %>
<%= form.select :model, Vehicles.model_options(@car.make), prompt: "Model" %>

That's it. A working dropdown, with zero setup and zero network calls. You can finally delete that hand-maintained MAKES = [...] constant.

Installation

Add it to your Gemfile:

gem "vehicles"

And run bundle install. That's the whole setup. No generator, no migration, no API key, no seed task β€” the dataset is bundled and loaded lazily into memory on first use.

[!NOTE] vehicles works in any Ruby project, not just Rails. The Rails bits (form option helpers, validators) light up automatically when Rails is present, and stay out of your way when it isn't.

The basics

Two methods cover 90% of what you need:

Vehicles.makes
# => ["Alfa Romeo", "Audi", "BMW", ...]   (alphabetical, ~47 makes)

Vehicles.models("Toyota")
# => ["Yaris", "Corolla", "Aygo", "RAV4", "C-HR", "Prius", ...]   (most common first)

Make lookups are forgiving by default β€” case-insensitive, slug-friendly, and alias-aware, so whatever your users type tends to land:

Vehicles.models("toyota")     # case-insensitive
Vehicles.models("VW")         # => Volkswagen   (common abbreviation)
Vehicles.models("merc")       # => Mercedes-Benz
Vehicles.models("alfa-romeo") # slug form

Unknown make? You get an empty array, never an exception:

Vehicles.models("DeLorean")   # => []

When you want the rich object instead of a plain list, ask for it:

make = Vehicles.make("Audi")     # => #<Vehicles::Make "Audi">
make.name                        # => "Audi"
make.slug                        # => "audi"
make.kinds                       # => [:car]
make.models                      # => ["A3", "A4", "Q3", "Q5", ...]
make.model("a3")                 # => #<Vehicles::Model "Audi A3">

find resolves a free-text "make + model" string into a single model β€” great for normalizing messy input:

Vehicles.find("vw golf")          # => #<Vehicles::Model "Volkswagen Golf">
Vehicles.find("Mercedes C Class") # => #<Vehicles::Model "Mercedes-Benz C-Class">
Vehicles.find("nope nope")        # => nil

search returns every model that matches, ranked β€” perfect for an autocomplete endpoint:

Vehicles.search("golf")
# => [#<Model "Volkswagen Golf">]

Vehicles.search("3")
# => [#<Model "BMW 3 Series">, #<Model "Mazda 3">, #<Model "CitroΓ«n C3">, ...]

Every Vehicles::Model reads like English and serializes cleanly:

car = Vehicles.find("seat leon")
car.make        # => "SEAT"
car.name        # => "Leon"
car.full_name   # => "SEAT Leon"
car.slug        # => "seat-leon"
car.to_h        # => { make: "SEAT", model: "Leon", slug: "seat-leon",
                #      kind: :car, body_type: :hatchback }

Kinds & body types

Every vehicle is classified on two axes, so you can filter, group, and label without maintaining your own taxonomy.

kind β€” what sort of vehicle it is. Sourced straight from official registration data:

:car  Β·  :motorcycle  Β·  :van  Β·  :truck  Β·  :pickup  Β·  :trailer  Β·  :bus  Β·  :moped  Β·  :quad

body_type β€” the sub-classification within a kind:

# cars         :hatchback :sedan :wagon :suv :mpv :coupe :convertible :roadster :pickup :van
# motorcycles  :naked :sport :adventure :trail :enduro :motocross :scooter :cruiser :touring
Vehicles.find("vw tiguan").kind        # => :car
Vehicles.find("vw tiguan").body_type   # => :suv
Vehicles.find("vw tiguan").suv?        # => true   ← predicate sugar for every body_type

Filter any list by either axis:

Vehicles.models("Toyota", body_type: :suv)   # => ["RAV4", "C-HR", "Yaris Cross", ...]
Vehicles.makes(kind: :car)                    # => makes that build cars  (the default)
Vehicles.makes(kind: :motorcycle)             # => makes that build bikes
Vehicles.search("golf").select(&:hatchback?)

[!NOTE] Today the bundled data is cars (kind: :car), each tagged with a body_type from the source registration data. kind and body_type are first-class on every record, so when motorcycle, van, and trailer packs land, the API β€” and your code β€” doesn't change. Market segments (supercar, sports car, city car, …) are an editorial layer that arrives with VehiclesDB.

Colors

A small canonical color palette ships with the gem β€” the handful of colors that cover virtually every car, plus an explicit other. It's the shared vocabulary for color dropdowns today and for color-accurate imagery from the hosted API later, so "red" means the same thing everywhere.

Vehicles.colors
# => [#<Color white "White" #F4F4F4>, #<Color black "Black" #1B1B1B>, ...]

Vehicles.color("navy")          # forgiving: synonyms, case, diacritics
# => #<Vehicles::Color blue "Blue" #27408B>
Vehicles.color("navy").slug     # => "blue"   ← the stable value you store
Vehicles.color("navy").hex      # => "#27408B" (representative swatch)

Vehicles.color_options          # => [["White", "white"], ["Black", "black"], ...]

Names are English β€” store the slug and localize the label in your app (the slug is also what maps to image color-variants down the line).

Rails dropdowns

The *_options helpers return [[label, value], ...] pairs, exactly what Rails' select wants:

Vehicles.make_options
# => [["Alfa Romeo", "alfa-romeo"], ["Audi", "audi"], ["BMW", "bmw"], ...]

Vehicles.model_options("audi")
# => [["A3", "a3"], ["A4", "a4"], ["Q3", "q3"], ...]

Dependent make β†’ model picker

The whole dataset is small, so the simplest, snappiest way to wire a dependent picker is client-side: embed Vehicles.catalog once and switch the model list with no request at all β€” no route, no controller, no fetch.

<div data-controller="vehicle-picker"
     data-vehicle-picker-catalog-value="<%= Vehicles.catalog.to_json %>">
  <%= form.select :make,  Vehicles.makes,             { include_blank: "Make" },
        data: { vehicle_picker_target: "make", action: "change->vehicle-picker#makeChanged" } %>
  <%= form.select :model, Vehicles.models(@car&.make), { include_blank: "Model" },
        data: { vehicle_picker_target: "model" } %>
</div>
// app/javascript/controllers/vehicle_picker_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["make", "model"]
  static values  = { catalog: Object }   // { "Audi": ["A3", ...], ... }

  makeChanged() {
    const models = this.catalogValue[this.makeTarget.value] || []
    const previous = this.modelTarget.value
    this.modelTarget.replaceChildren(new Option("Model", ""))
    for (const name of models) {
      this.modelTarget.add(new Option(name, name, false, name === previous))
    }
  }
}

That's the whole picker β€” the gem ships the data, you keep your markup and ~12 lines of JS. (Storing slugs instead of names? Swap the selects for make_options / model_options and key the catalog by slug.)

[!TIP] Prefer a server round-trip (huge/remote dataset, or you'd rather not embed it)? It's a three-line endpoint β€” render json: Vehicles.models(params[:make]) β€” and the controller fetches that on makeChanged instead of reading the embedded catalog. Same gem API, your call.

Validations

Drop-in ActiveModel validators, so bad data never reaches your database:

class Car < ApplicationRecord
  validates :make,  vehicle_make: true                 # must be a real make
  validates :model, vehicle_model: { make: :make }     # must be a real model of that make
end
Car.new(make: "Volkswagen", model: "Golf").valid?      # => true
Car.new(make: "Volkswagen", model: "Mustang").valid?   # => false
Car.new(make: "Tesler",     model: "Model 3").valid?   # => false

They're forgiving the same way the lookups are (aliases, case, slugs), and they never raise on blank/garbage input β€” they just add an error.

Here's the pattern that works cleanly end to end β€” and a reference schema for the columns a vehicle record actually needs.

A record stores its vehicle's identity, not the reference data. Make, model, year, and color identify the car; kind and body_type (and, later, specs/images) are properties of that make+model owned by the dataset β€” look them up via the gem, don't duplicate them per row (they'd just drift). So the schema is small and stable:

Column Type Store Example
make string display name "Volkswagen"
model string display name "Golf"
year integer the year 2022
color string a canonical color slug "blue"

Everything else (kind, body_type, production years, images, …) is derived: Vehicles.model(record.make, record.model). No kind/body_type columns, no migration to add new metadata later β€” it lands in the dataset, not your schema.

Store the display name. The simplest, most readable thing to persist is the name itself ("Volkswagen", "Golf"). Populate the selects with the plain name lists, where the option value is the name:

<%= form.select :make,  Vehicles.makes,             { include_blank: "Make" } %>
<%= form.select :model, Vehicles.models(@car.make), { include_blank: "Model" } %>
<%= form.select :year,  (Date.current.year + 1).downto(1990).to_a, { include_blank: "Year" } %>
<%= form.select :color, Vehicles.color_options,     { include_blank: "Color" } %>

[!TIP] Use Vehicles.makes / Vehicles.models (name = value) when your column stores the display name. Use Vehicles.make_options / Vehicles.model_options (which pair [name, slug]) only when you store the slug β€” then read the name back with Vehicles.make(slug).name. Pick one and stay consistent.

Validate what you store β€” the dropdowns already constrain input, but the validators guard every other write path (imports, console, your own API):

validates :make,  vehicle_make: true,             allow_blank: true
validates :model, vehicle_model: { make: :make }, allow_blank: true
validates :color, inclusion: { in: Vehicles.colors.map(&:slug) }, allow_blank: true

Read the metadata back from a stored pair whenever you need it β€” kind, body type, and (with the hosted API) years/segment/image:

car = Vehicles.model(record.make, record.model)   # => Vehicles::Model | nil
car&.body_type                                      # => :hatchback
car&.suv?                                           # => false

Only accept the kinds you support. Filter every list/lookup by kind: so the picker only ever offers vehicles your app handles β€” and widening later (when new kind packs ship, or when you decide to accept them) is a one-word change, not a migration:

Vehicles.makes(kind: :car)                 # makes that build cars
Vehicles.models("Toyota", kind: :car)      # ...their cars only

No migration, no seed task, no API key β€” the data is bundled and read from memory. The whole integration is the markup above plus a 3-line endpoint for the dependent model list (see Rails dropdowns).

Country & region awareness

Vehicle availability is regional β€” a Vauxhall in the UK is an Opel on the continent, a Holden only ever shipped in Australia. vehicles is built around this from day one:

Vehicles.makes(region: :eu)             # makes sold in the EU  (the default today)
Vehicles.models("Toyota", region: :eu)

[!NOTE] Today the bundled data covers the EU market (~47 makes, ~460 model nameplates, sourced from the Dutch national vehicle register β€” see Where the data comes from). :us, :gb, :au, :nz, and :ca packs are on the roadmap. The region API is already in place, so adding them is additive, never a breaking change.

Set your app's default once:

Vehicles.configure do |config|
  config.region = :eu
end

πŸ”“ More with VehiclesDB

vehicles is the free, open-source SDK for VehiclesDB β€” a hosted API for richer vehicle data. The gem is fully standalone and always will be; pointing it at VehiclesDB is purely additive. Drop in an API key and the same objects you already use light up with more:

Vehicles.configure do |config|
  config.api_key = ENV["VEHICLESDB_API_KEY"]   # optional β€” unlocks hosted data
end

car = Vehicles.find("vw golf")
car.years                       # => 1974..2024            production years
car.segment                     # => :hatchback / :hot_hatch   editorial segment
car.image                       # => "https://cdn.vehiclesdb.com/volkswagen/golf.webp"
car.image(year: 2020)           # year-accurate photo
car.image(year: 2020, color: :silver)

Under the hood this is a simple provider model: a LocalProvider (the bundled data) is always available, and a HostedProvider activates only when an API key is configured. Calls prefer the hosted data when it's there and fall back to the local data otherwise β€” never raising, never blocking your request:

car.image     # no API key configured?  => nil   (your views just render a placeholder)
car.years     # not in the local pack yet? => nil, until you add a key or we ship it locally

So you can build your UI against the full API today, ship on the free local data, and flip on richer data later without touching your code.

Configuration

Everything has a sensible default; you can run the gem without configuring anything. When you do want to tune it:

# config/initializers/vehicles.rb
Vehicles.configure do |config|
  config.region    = :eu                          # default region for queries
  config.api_key   = ENV["VEHICLESDB_API_KEY"]    # optional hosted VehiclesDB data
  config.aliases   = { "Chevy" => "Chevrolet" }   # add your own make aliases
  config.use_cache = true                         # prefer a refreshed dataset over the bundled one
end

Staying current (optional)

The bundled snapshot works offline forever β€” but vehicle data changes (new makes, fixes), and you shouldn't have to ship a gem upgrade to every app just to get them. So vehicles can refresh from the published dataset into a local file cache; loads prefer the cache over the bundled copy. Data fixes reach your app without a gem bump.

rails g vehicles:install drops a VehiclesRefreshJob β€” schedule it (daily is plenty), e.g. with solid_queue:

# config/recurring.yml
vehicles_refresh:
  class: VehiclesRefreshJob
  schedule: every day at 3am

Or refresh manually:

Vehicles.refresh!     # pull latest -> cache; true/false, never raises
Vehicles.data_version # => the version now in effect (cached, or bundled)

It's safe by design: a failed/partial download never replaces good data, and the gem keeps serving the cache (or the bundled snapshot) no matter what. Want it fully offline/deterministic? config.use_cache = false.

The full Ruby API

# Lists (strings β€” drop straight into a form)
Vehicles.makes                          # => [String]
Vehicles.makes(kind: :car, region: :eu)
Vehicles.models("Audi")                 # => [String]
Vehicles.models("Audi", body_type: :suv)

# Option pairs (for Rails `select`)
Vehicles.make_options                   # => [[label, value]]
Vehicles.model_options("Audi")          # => [[label, value]]

# Whole make => [models] map (embed once for a client-side dependent picker)
Vehicles.catalog(kind: :car)            # => { "Audi" => ["A3", ...], ... }

# Objects
Vehicles.make("Audi")                   # => Vehicles::Make | nil
Vehicles.find("audi a3")                # => Vehicles::Model | nil  (one free-text string)
Vehicles.model("Audi", "A3")            # => Vehicles::Model | nil  (a stored make+model pair)
Vehicles.search("a3")                   # => [Vehicles::Model]

# Vehicles::Make
make.name      make.slug      make.aliases     make.kinds
make.models    make.model("a3")                make.to_h

# Vehicles::Model
model.make     model.name     model.full_name  model.slug    model.to_h
model.kind     model.body_type                 model.suv?    model.coupe?   # …predicates
model.years    model.segment  model.image(year:, color:)     # ← hosted VehiclesDB API

# Colors (canonical palette)
Vehicles.colors                         # => [Vehicles::Color]
Vehicles.color("navy")                  # => Vehicles::Color | nil (forgiving)
Vehicles.color_options                  # => [[name, slug]]  (for select)
color.slug     color.name     color.hex

# Meta
Vehicles.data_version                   # => "2026.06.0"   (version in effect: cached or bundled)
Vehicles.region                         # => :eu

# Refresh (optional β€” keep data current without a gem upgrade)
Vehicles.refresh!                       # pull latest published data -> cache; true/false, never raises
Vehicles.reload!                        # drop the in-memory dataset (reload from disk)

Where the data comes from

The bundled dataset is built from RDW Open Data β€” the Dutch national vehicle register, effectively a census of every vehicle on EU roads β€” aggregated to clean nameplates (trims and generations collapsed: one "Golf", one "3 Series"), ranked by how many are actually registered, with the long tail of kit cars and gray imports filtered out.

Every record is shaped like this:

{
  "name": "Volkswagen", "slug": "volkswagen", "kinds": ["car"],
  "models": [
    { "name": "Golf",   "slug": "golf",   "kind": "car", "body_type": "hatchback" },
    { "name": "Tiguan", "slug": "tiguan", "kind": "car", "body_type": "suv" },
    { "name": "Touran", "slug": "touran", "kind": "car", "body_type": "mpv" }
  ]
}
  • kind is sourced, not guessed β€” straight from RDW's voertuigsoort, the same classification the government uses. body_type is curated on top of RDW's inrichting (which is too coarse to use raw β€” it lumps wagons, SUVs and crossovers together), mapped to a clean canonical vocabulary.
  • Nameplate-level, on purpose. "Golf" covers the GTI, the Variant, the R. "3 Series" covers every 3-series trim. That's what a dropdown wants. Per-trim and per-generation detail is a VehiclesDB API concern.
  • Source data: RDW Open Data, licensed CC0.
  • This dataset (data/vehicles.json): CC-BY 4.0. Attribution: "Vehicle data from VehiclesDB, derived from RDW Open Data."
  • The gem code: MIT.

The data is versioned (Vehicles.data_version). Each gem release bundles a known snapshot β€” the offline, deterministic floor. To get newer data, either upgrade the gem or enable refresh (which pulls published releases without a gem bump). Either way it's explicit and versioned β€” no silent mutations.

How it works

No magic, just good defaults:

  • Bundled, not fetched. data/vehicles.json is packaged in the gem and loaded into a frozen, memoized in-memory index on first access. First call builds the index; every call after is a hash lookup. No HTTP, no SQLite, no ActiveRecord on the read path.
  • Zero-config. No initializer required. No migration. Nothing to schedule.
  • Standalone first, SDK second. The hosted VehiclesDB layer is strictly optional and detected at runtime; the gem never hard-depends on it.
  • Rails-aware, not Rails-bound. Form helpers and validators register through a lightweight Railtie only when Rails is loaded.

Roadmap

This is a young gem. What's bundled today is EU car make/model data (with kind + body_type) and the API above. On the way:

  • 🏍️ More kinds: motorcycles, vans, trucks, trailers β€” the shape already supports them
  • 🌍 More regions: :us (NHTSA vPIC), :gb (DVSA), :au, :nz, :ca
  • πŸ“… Production years in the local data
  • πŸ–ΌοΈ Model images, year-accurate and color variants (via VehiclesDB)
  • 🏷️ Segments (supercar, sports car, city car, hot hatch) and richer metadata
  • πŸ”Ž A mountable autocomplete endpoint so the dependent-dropdown recipe becomes one line

Want one of these sooner? Open an issue.

Testing

Run the test suite with bundle exec rake test. The gem is tested against multiple Rails versions with Appraisal: bundle exec appraisal rails-8.0 rake test.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/rameerez/vehicles. Our code of conduct is: just be nice and make your mom proud of what you do and post online.

License

The gem is available as open source under the terms of the MIT License. The bundled dataset is licensed CC-BY 4.0 (see Where the data comes from).