
The deep-search experiment that grew into VietRailway’s search page: station, carriage, seat class, seat type, and price ceiling on one form, with "clear filters", "my tickets", and sort — plus a results card that explains its own price.
The form that forced a schema. Eight fields can be combined 8! ways — the backend could not special-case each one, so the query had to be built, not written:
function buildTripQuery(f: SearchFilters) {
return {
where: {
route: { stations: { some: { name: f.from ?? undefined } } },
departsAt: f.date ? { gte: startOfDay(f.date) } : undefined,
classes: { some: { type: f.seatType ?? undefined } },
minPrice: { lte: f.maxPrice ?? Number.MAX_SAFE_INTEGER },
},
orderBy: f.sort === 'price' ? { minPrice: 'asc' } : undefined,
};
}
What went right: the results card states why it is cheap — "from 450,000 ₫, 2 carriage/class configurations" — so the cheapest result never looks like an error.
What went wrong: placeholder-as-label inputs. "For example: Hà Nội" as a label is a hint wearing a costume; real labels + <datalist> suggestions beat clever placeholders every time.