The problem
Buying a car means visiting dozens of websites. Manufacturer sites show specs differently. Regional pricing varies wildly. Comparing a Toyota to a Honda means opening multiple tabs and manually cross referencing data that doesn't quite match up.
I wanted to compare cars the same way I'd compare laptops on a spec comparison site. Side by side, with consistent data points, across different markets. That platform didn't exist.
What I built
JustCar aggregates data from 49,000+ vehicles across 219 brands. Users can compare up to 4 cars side by side with 50+ specifications, filter by body type, and see pricing across multiple markets including Vietnam, Philippines, Japan, UK, India, Australia, Singapore, EU, and Canada.
The journey
I thought the hard part would be building scrapers. I was wrong. Scraping was straightforward. The real challenge was making sense of data that was never meant to be compared.
Tech stack
| Layer | Technology | Why |
|---|---|---|
| Frontend | Next.js 16 | App Router, ISR for SEO |
| Language | TypeScript | Type safety for complex data |
| Database | Supabase | PostgreSQL with full text search |
| Validation | Zod | Runtime schema validation |
| Styling | Tailwind CSS 4 | Utility first, fast iteration |
| Hosting | Vercel | Edge caching, easy deploys |
What building this taught me
1. Data quality is the product
I spent 70% of my time on data cleaning and normalization, 30% on the UI. Users don't care about your beautiful interface if the specs are wrong.
One site lists horsepower as "300 hp," another as "300HP," another as "224 kW." Engine displacement might be "2.0L" or "1998cc" or "2000 cm³." Making these comparable required building a normalization pipeline more complex than the UI itself.
// Real example of variations I had to handle
const horsepowerVariations = [
'300 hp',
'300HP',
'300 bhp',
'300ps',
'224 kW',
'223.71kW',
'224kw',
]
// Normalized to:
interface NormalizedPower {
value: number // 300
unit: 'hp' // standardized
kw: number // 224 (always include kW conversion)
}
Resources that helped:
- Zod Documentation for schema validation
- PostgreSQL Data Types for storage decisions
2. Scrapers are never done
I thought: build scraper, run scraper, done. Reality: websites change constantly.
A site redesigns, your scraper breaks. A site adds anti bot measures, your scraper breaks. A site changes their data format, your scraper breaks.
I learned to treat scraper maintenance as ongoing work, not a one time setup. I built monitoring with Sentry so I know within hours when something breaks.
Scraper Health Dashboard:
┌────────────────┬────────┬──────────┐
│ Source │ Status │ Last Run │
├────────────────┼────────┼──────────┤
│ AutoTrader UK │ ✓ │ 2h ago │
│ CarGurus │ ✓ │ 3h ago │
│ AutoData PH │ ⚠ │ 1d ago │ ← needs attention
│ CarQuery API │ ✓ │ 6h ago │
└────────────────┴────────┴──────────┘
3. PostgreSQL full text search is underrated
I almost added Elasticsearch because that's what "real" search uses. Then I tried PostgreSQL's built in full text search with trigram indexes.
It handled 49,000+ records beautifully. The realization: sometimes the boring choice is the right choice. Not everything needs a specialized tool.
-- PostgreSQL trigram search that replaced Elasticsearch
CREATE INDEX idx_cars_search ON cars
USING gin (name gin_trgm_ops, brand gin_trgm_ops);
-- Fast fuzzy search
SELECT * FROM cars
WHERE name % 'camery' -- finds "Camry" despite typo
ORDER BY similarity(name, 'camery') DESC;
Resources:
4. Multi market complexity compounds
A "base model" in one market might have features that are "premium" elsewhere. The same car might have different names in different regions. Prices in different currencies need conversion but also contextual understanding.
Is $30,000 expensive for this car? Depends on the market. This complexity kept revealing itself in layers.
The bigger realization
Building JustCar taught me that aggregation products live and die by data quality. I could have built the world's best UI, but if users find one wrong spec, they lose trust in everything.
The unsexy work of validation, cleaning, and monitoring is what makes the product valuable. Features are easy. Trustworthy data is hard.
What I would do differently
Build an admin dashboard from day one. I managed data through scripts and direct database queries for months. A proper admin interface for reviewing flagged data, fixing issues, and monitoring scraper health would have saved hours of terminal work every week.
References
- Next.js ISR Documentation
- Supabase Documentation
- PostgreSQL Trigram Extension
- Zod Schema Validation
- Sentry Error Monitoring
Links
- Live Site: justcar.io
