Inside Travel-Booking-Web-Application-MERN: One Booking Model, Two Travel Worlds
A clean MERN clone that uses polymorphic Mongoose references, role-based middleware, and a thin React client to make flights and hotels feel like one system.
- The repo’s strongest idea is a single Booking model that can resolve to flights or hotels without splitting the app into duplicate domains.
- Role-based middleware is not a side detail here. It is what keeps one shared inventory system safe for both customers and admins.
- The frontend stays intentionally thin, using Axios interceptors and React Context to consume the API instead of recreating business logic in the client.
- The AdminDashboard is the stress test for the whole design, because it has to manage multiple entity types without breaking the abstraction.
The Clever Part Is the Data Model
Most travel apps begin with the surface: search, filter, book, repeat. This repo is more interesting because the real decision happens underneath the UI. It treats flights and hotels as different inventory types, but only one kind of booking.
The center of gravity is the Mongoose refPath setup in Booking.js. One itemId field can point to either a Flight or a Hotel document, depending on itemType. That means the application does not need separate booking collections, separate booking controllers, or separate history pages just to preserve domain differences.
| Approach | Data model complexity | Controller complexity | Admin maintenance | Extensibility | Risk of duplication |
|---|---|---|---|---|---|
| Separate FlightBooking and HotelBooking collections | High | High | Two admin flows to maintain | Low | High |
| One Booking collection with refPath | Low | Low | One shared booking layer | High | Low |
A Travel App With Two Audiences
The repo is really two products sharing one backend contract. For travelers, it is a place to browse flights and hotels, search with a little fuzziness, and submit bookings. For admins, it is an inventory console that creates, edits, and deletes the same entities the user sees.
| Audience | Primary job | Visible surface | Hidden dependency |
|---|---|---|---|
| Traveler | Search and book trips | Flights and hotel listings | Auth, booking writes, booking history |
| Admin | Manage inventory and bookings | Admin dashboard and forms | RBAC, CRUD routes, shared models |
// server/models/Booking.js
const bookingSchema = new mongoose.Schema({
itemType: { type: String, required: true, enum: ['Flight', 'Hotel'] },
itemId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
refPath: 'itemType'
}
});
// later
Booking.find({ userId }).populate('itemId');
That last line is the elegance test. A single populate() call can bring back the right document type because the schema already told Mongoose where to look. The code stays small. The mental model stays coherent.
Why RBAC Matters More Than It Looks
The security layer is doing architectural work, not just gatekeeping. authMiddleware verifies the JWT and attaches the user. roleMiddleware decides whether the request can mutate inventory. Public reads stay public. Admin writes stay fenced off.
// server/middleware/authMiddleware.js
const authMiddleware = async (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'No token' });
req.user = jwt.verify(token, process.env.JWT_SECRET);
next();
};
const roleMiddleware = (...roles) => (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: 'Forbidden' });
}
next();
};
That split is what makes the rest of the app feel safe. Without it, the shared booking model would be easy to read and hard to trust.
The Frontend Stays Thin on Purpose
The React side does not try to become a second source of truth. Axios interceptors inject the JWT automatically, so individual components do not need to know how auth headers work. React Context carries user state across the app without dragging in Redux-level ceremony.
// client/src/services/api.js
api.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
That choice keeps the client readable. The pages can focus on rendering forms, tabs, and booking summaries instead of rebuilding networking logic in every view.
The Admin Dashboard Is the Pressure Test
If the abstraction is real, it survives the admin dashboard. If it is fake, this is where it breaks. AdminDashboard.jsx has to switch between flights, hotels, and bookings while still feeling like one interface.
| Trait | What it buys | What it costs |
|---|---|---|
| Single admin surface | Less context switching | More conditional rendering |
| Reusable item form | Fewer duplicate forms | More branching inside one component |
| Unified booking view | Cleaner oversight | A bigger component to reason about |
The tradeoff is obvious and acceptable. Yes, it can become a God Component. But it is also a practical proof that the data model can hold the product together.
Why This Feels Like a Modern Boilerplate, Not a Tutorial Fossil
The stack choices are pragmatic. Vite replaces older bootstrap defaults. React Context stands in for heavier global state. JWTs live in localStorage for a consumer-friendly session experience. Search uses regex matching to feel forgiving without introducing another service.
| Choice | Why it matters |
|---|---|
| Vite | Fast feedback loops and a modern starter experience |
| Context over Redux | Less boilerplate for a small-to-mid scale app |
| JWT in localStorage | Simple persistent auth for a booking flow |
| Regex search | Good-enough fuzzy discovery without extra infrastructure |
None of that is flashy. That is the point. The repo wants to be easy to clone, easy to understand, and easy to extend.
What This Repo Is Really For
This is a strong reference implementation and portfolio piece. It is not pretending to be a production travel platform with payments, real inventory feeds, or enterprise-grade search. Its value is narrower and more useful: it shows how far a disciplined MERN stack can go when one smart data model keeps the whole app aligned.
A full stack travel booking web application built using MERN stack.