Appearance
Car Hierarchy — Frontend Developer Guide
Full 4-level car reference data is now imported into the dev database and available through the API. This guide covers the data model, endpoints, response shapes, enum values, and implementation patterns for building the YMMM (Year/Make/Model/Modification) cascade filter and related UI.
Data Summary
| Table | Records | Description |
|---|---|---|
car_makes | 124 | Brands: BMW, Toyota, Audi, etc. |
car_models | 4,795 | Models grouped per make: 3 Series, Camry, A4 |
car_generations | 7,207 | Year-based generations: "2018-Present", "2007-2014" |
car_modifications | 30,066 | Engine/transmission variants with full specs |
Entity Relationship
car_makes (124)
└── car_models (4,795) — FK: make_id
└── car_generations (7,207) — FK: model_id
└── car_modifications (30,066) — FK: generation_idEach level depends on its parent. The frontend loads them in cascade: user picks a make, then models load, then generations, then modifications.
Public API Endpoints (no auth required)
Base: /api/store/cars/...
All responses are cached server-side for 1 hour.
1. List Makes
GET /api/store/cars/makes
GET /api/store/cars/makes?popular=trueResponse:
json
{
"data": [
{
"id": 12,
"name": "BMW",
"slug": "bmw",
"logo_url": null,
"is_popular": false
},
{
"id": 100,
"name": "Toyota",
"slug": "toyota",
"logo_url": null,
"is_popular": false
}
]
}| Field | Type | Notes |
|---|---|---|
id | number | SERIAL PK, use for next cascade call |
name | string | Display name, title-cased |
slug | string | URL-friendly, unique |
logo_url | string | null | Not populated yet |
is_popular | boolean | Use ?popular=true to filter |
2. List Models by Make
GET /api/store/cars/makes/{makeId}/modelsExample: GET /api/store/cars/makes/12/models (BMW)
Response:
json
{
"data": [
{
"id": 418,
"make_id": 12,
"name": "1 Series",
"slug": "1-series"
},
{
"id": 420,
"make_id": 12,
"name": "3 Series",
"slug": "3-series"
}
]
}| Field | Type | Notes |
|---|---|---|
id | number | Use for next cascade call |
make_id | number | Parent make FK |
name | string | Display name |
slug | string | URL-friendly |
3. List Generations by Model
GET /api/store/cars/models/{modelId}/generationsExample: GET /api/store/cars/models/420/generations (BMW 3 Series)
Response:
json
{
"data": [
{
"id": 663,
"model_id": 420,
"name": "2019-Present",
"code": null,
"year_from": 2019,
"year_to": null,
"steering": null
},
{
"id": 664,
"model_id": 420,
"name": "2012-2018",
"code": null,
"year_from": 2012,
"year_to": 2018,
"steering": null
}
]
}| Field | Type | Notes |
|---|---|---|
id | number | Use for next cascade call |
model_id | number | Parent model FK |
name | string | Typically year range: "2019-Present", "2012-2018" |
code | string | null | Generation code (E46, F30, etc.) — currently null for imported data |
year_from | number | null | Start year |
year_to | number | null | End year; null = still in production |
steering | string | null | "left", "right", "both", "universal", or null |
4. List Modifications by Generation
GET /api/store/cars/generations/{generationId}/modificationsExample: GET /api/store/cars/generations/663/modifications
Response:
json
{
"data": [
{
"id": 27111,
"generation_id": 663,
"name": "1.8L 5MT FWD (90 HP)",
"engine_volume": "1.8",
"fuel_type": "petrol",
"power": 90,
"transmission": "mt",
"drivetrain": "fwd"
},
{
"id": 27112,
"generation_id": 663,
"name": "2.0L 5MT FWD (107 HP)",
"engine_volume": "2.0",
"fuel_type": "petrol",
"power": 107,
"transmission": "mt",
"drivetrain": "fwd"
},
{
"id": 27113,
"generation_id": 663,
"name": "1.8L TD 5MT FWD (73 HP)",
"engine_volume": "1.8",
"fuel_type": "diesel",
"power": 73,
"transmission": "mt",
"drivetrain": "fwd"
}
]
}| Field | Type | Notes |
|---|---|---|
id | number | Modification PK |
generation_id | number | Parent generation FK |
name | string | Human-readable label (max 150 chars), e.g. "2.0L 4cyl 6AT (150 HP)" |
engine_volume | string | null | Liters as decimal string: "1.6", "2.0", "3.5" |
fuel_type | string | null | Enum — see below |
power | number | null | Horsepower (HP) |
transmission | string | null | Enum — see below |
drivetrain | string | null | Enum — see below |
Enum Values
fuel_type
| Value | Label (RU) | Count |
|---|---|---|
petrol | Бензин | 20,324 |
diesel | Дизель | 8,454 |
hybrid | Гибрид | 664 |
electric | Электро | 445 |
gas | Газ | 149 |
transmission
| Value | Label (RU) | Count |
|---|---|---|
mt | Механика | 16,260 |
at | Автомат | 11,237 |
robot | Робот (DSG/S-Tronic) | 995 |
cvt | Вариатор | 779 |
amt | AMT | 1 |
drivetrain
| Value | Label (RU) | Count |
|---|---|---|
fwd | Передний | 16,637 |
rwd | Задний | 7,079 |
awd | Полный | 6,184 |
4wd | 4WD | 0 (exists in enum, not in current dataset) |
steering (on generations)
| Value | Label (RU) |
|---|---|
left | Левый |
right | Правый |
both | Оба |
universal | Универсальный |
Zod Schemas
ts
import { z } from 'zod'
// Enums
export const FuelType = z.enum(['petrol', 'diesel', 'hybrid', 'electric', 'gas'])
export const Transmission = z.enum(['at', 'mt', 'cvt', 'amt', 'robot'])
export const Drivetrain = z.enum(['fwd', 'rwd', 'awd', '4wd'])
export const Steering = z.enum(['left', 'right', 'both', 'universal'])
// Entities
export const CarMakeSchema = z.object({
id: z.number(),
name: z.string(),
slug: z.string(),
logo_url: z.string().nullable(),
is_popular: z.boolean(),
})
export const CarModelSchema = z.object({
id: z.number(),
make_id: z.number(),
name: z.string(),
slug: z.string(),
})
export const CarGenerationSchema = z.object({
id: z.number(),
model_id: z.number(),
name: z.string(),
code: z.string().nullable(),
year_from: z.number().nullable(),
year_to: z.number().nullable(),
steering: Steering.nullable(),
})
export const CarModificationSchema = z.object({
id: z.number(),
generation_id: z.number(),
name: z.string(),
engine_volume: z.string().nullable(),
fuel_type: FuelType.nullable(),
power: z.number().nullable(),
transmission: Transmission.nullable(),
drivetrain: Drivetrain.nullable(),
})
// Type exports
export type CarMake = z.infer<typeof CarMakeSchema>
export type CarModel = z.infer<typeof CarModelSchema>
export type CarGeneration = z.infer<typeof CarGenerationSchema>
export type CarModification = z.infer<typeof CarModificationSchema>API Functions (FSD: entities/car/api/)
ts
// entities/car/api/index.ts
import type { CarMake, CarModel, CarGeneration, CarModification } from '../model'
export async function fetchCarMakes(popular?: boolean): Promise<CarMake[]> {
const params = popular ? '?popular=true' : ''
const { data } = await $fetch<{ data: CarMake[] }>(`/api/store/cars/makes${params}`)
return data
}
export async function fetchCarModels(makeId: number): Promise<CarModel[]> {
const { data } = await $fetch<{ data: CarModel[] }>(
`/api/store/cars/makes/${makeId}/models`,
)
return data
}
export async function fetchCarGenerations(modelId: number): Promise<CarGeneration[]> {
const { data } = await $fetch<{ data: CarGeneration[] }>(
`/api/store/cars/models/${modelId}/generations`,
)
return data
}
export async function fetchCarModifications(generationId: number): Promise<CarModification[]> {
const { data } = await $fetch<{ data: CarModification[] }>(
`/api/store/cars/generations/${generationId}/modifications`,
)
return data
}Cascade Filter Composable
ts
// widgets/ymm-filter/model/useCarCascade.ts
export function useCarCascade() {
const selectedMakeId = ref<number | null>(null)
const selectedModelId = ref<number | null>(null)
const selectedGenerationId = ref<number | null>(null)
const selectedModificationId = ref<number | null>(null)
const makes = ref<CarMake[]>([])
const models = ref<CarModel[]>([])
const generations = ref<CarGeneration[]>([])
const modifications = ref<CarModification[]>([])
// Load makes on mount
onMounted(async () => {
makes.value = await fetchCarMakes()
})
// When make changes → load models, reset downstream
watch(selectedMakeId, async (makeId) => {
models.value = []
generations.value = []
modifications.value = []
selectedModelId.value = null
selectedGenerationId.value = null
selectedModificationId.value = null
if (makeId) {
models.value = await fetchCarModels(makeId)
}
})
// When model changes → load generations, reset downstream
watch(selectedModelId, async (modelId) => {
generations.value = []
modifications.value = []
selectedGenerationId.value = null
selectedModificationId.value = null
if (modelId) {
generations.value = await fetchCarGenerations(modelId)
}
})
// When generation changes → load modifications
watch(selectedGenerationId, async (genId) => {
modifications.value = []
selectedModificationId.value = null
if (genId) {
modifications.value = await fetchCarModifications(genId)
}
})
return {
makes, models, generations, modifications,
selectedMakeId, selectedModelId, selectedGenerationId, selectedModificationId,
}
}UI Implementation Notes
Generation display
Generation name is typically a year range string ("2019-Present", "2012-2018"). For display, combine with year_from/year_to fields for formatting flexibility:
ts
function formatGeneration(gen: CarGeneration): string {
if (gen.year_from && gen.year_to) return `${gen.year_from}–${gen.year_to}`
if (gen.year_from) return `${gen.year_from}–н.в.` // "н.в." = настоящее время
return gen.name
}Modification display
The name field is already human-readable ("2.0L 4cyl 6AT (150 HP)"). For richer UI, use the parsed fields:
ts
function formatModification(mod: CarModification): string {
const parts: string[] = []
if (mod.engine_volume) parts.push(`${mod.engine_volume}L`)
if (mod.fuel_type) parts.push(fuelTypeLabel(mod.fuel_type))
if (mod.power) parts.push(`${mod.power} л.с.`)
if (mod.transmission) parts.push(transmissionLabel(mod.transmission))
if (mod.drivetrain) parts.push(drivetrainLabel(mod.drivetrain))
return parts.join(' / ') || mod.name
}i18n label maps
ts
export const fuelTypeLabels: Record<string, string> = {
petrol: 'Бензин',
diesel: 'Дизель',
hybrid: 'Гибрид',
electric: 'Электро',
gas: 'Газ',
}
export const transmissionLabels: Record<string, string> = {
mt: 'Механика',
at: 'Автомат',
cvt: 'Вариатор',
amt: 'АМТ',
robot: 'Робот',
}
export const drivetrainLabels: Record<string, string> = {
fwd: 'Передний',
rwd: 'Задний',
awd: 'Полный',
'4wd': '4WD',
}Where This Data Is Used
| Page / Feature | How car data is used |
|---|---|
| Catalog filters | YMMM cascade dropdowns to narrow product search |
| Add listing (seller) | Seller selects make → model → generation → modification for part compatibility |
| Product card | Shows compatible cars: "BMW 3 Series 2019–Present, 2.0L Petrol AT FWD" |
| Product search | Filter by make/model/generation/modification via query params |
| SEO URLs | Use slug fields for routes: /cars/bmw/3-series |
Error Handling
| Status | Code | When |
|---|---|---|
| 404 | not_found | Invalid make/model/generation ID in URL |
Example:
json
{
"error": {
"code": "not_found",
"message": "Car make not found"
}
}Dev Environment
Test the live endpoints (requires HTTP Basic Auth on dev):
bash
# List all makes
curl -u $PARTIZAP_BASIC_AUTH_USER:$PARTIZAP_BASIC_AUTH_PASS https://dev.partizap.ru/api/store/cars/makes
# Models for BMW (id=12)
curl -u $PARTIZAP_BASIC_AUTH_USER:$PARTIZAP_BASIC_AUTH_PASS https://dev.partizap.ru/api/store/cars/makes/12/models
# Generations for a model
curl -u $PARTIZAP_BASIC_AUTH_USER:$PARTIZAP_BASIC_AUTH_PASS https://dev.partizap.ru/api/store/cars/models/420/generations
# Modifications for a generation
curl -u $PARTIZAP_BASIC_AUTH_USER:$PARTIZAP_BASIC_AUTH_PASS https://dev.partizap.ru/api/store/cars/generations/663/modifications