Not every project needs a database and a backend framework. When I built Travel Sync, a trip planning and itinerary management web app, I used plain HTML, CSS, and JavaScript with JSON for data — no backend, no build tools. Here's how I structured the data so the app stayed manageable as features grew, and what I'd suggest if you're building something similar.
The Stack
- Frontend: HTML, CSS, JavaScript (no framework)
- Data: JSON, structured and persisted client-side
- Deployment: Render, with version control on GitHub
Step 1: Model Your Data Before Writing UI Code
The core challenge in a trip planner is nesting: a trip has multiple destinations, each destination has multiple days, and each day has multiple activities. It's tempting to just start building forms and figure out storage later — don't. I started by writing out the shape of the data first:
{"tripId":"t1","tripName":"Goa Weekend","destinations":[{"name":"Goa","days":[{"date":"2026-09-12","activities":[{"time":"09:00","title":"Beach visit","notes":"Bring sunscreen"}]}]}]}
Lesson: once you know the nesting, the UI components almost design themselves — a trip list, a destination view, a day view, an activity list. Data shape drives component shape.
Step 2: Keep One Source of Truth
Early versions of Travel Sync had trip data scattered across multiple JS variables that got out of sync — editing an activity in one place didn't update the summary view elsewhere. The fix was boring but important: one JSON object in memory as the single source of truth, with every UI update reading from and writing back to it.
let tripData = loadTripsFromStorage();
function updateActivity(tripId, dayIndex, activityIndex, newActivity) {
const trip = tripData.find(t => t.tripId === tripId);
trip.destinations[0].days[dayIndex].activities[activityIndex] = newActivity;
saveTripsToStorage(tripData);
renderTrip(tripId);
}
Lesson: in any app without a backend, decide early where the "truth" lives, and make every function read/write through it instead of touching the DOM directly.
Step 3: Re-render Instead of Patching the DOM by Hand
Without a framework, it's tempting to manually update individual DOM elements when data changes. This gets unmanageable fast once you have nested lists (trips → destinations → days → activities). Instead, I rendered each section from the current data state every time something changed:
function renderTrip(tripId) {
const trip = tripData.find(t => t.tripId === tripId);
const container = document.getElementById("trip-view");
container.innerHTML = trip.destinations
.map(d => renderDestination(d))
.join("");
}
It's less "efficient" than surgical DOM updates, but for an app this size, it's far easier to reason about — the UI always matches the data because it's rebuilt from the data.
Lesson: for small-to-medium apps, "re-render the whole section from data" beats hand-patching the DOM. Optimize only if you actually hit a performance problem.
Step 4: Design Mobile-First from the Start
Since people plan trips on their phones as much as on desktops, I built the layout mobile-first — single-column day views, collapsible destination sections — then expanded the CSS for larger screens rather than the reverse. Retrofitting mobile support onto a desktop-first layout is almost always more painful than starting mobile-first.
What I'd Tell Someone Starting This Today
- Model your nested data structure before writing any UI.
- Keep a single source of truth and update everything through it.
- Re-render from data instead of manually patching the DOM.
- Build mobile-first, especially for anything travel-related.
You don't need a framework or a backend to build something real — you just need a clear data model and some discipline about where state lives.
Travel Sync is a trip planning and itinerary management web app built with HTML, CSS, JavaScript, and JSON. Live demo here.