Travel Booking Platform
A web and mobile platform for booking tours, hotels, flights and car rentals, with MoMo payment integration.
- React Native
- ReactJS
- Node.js
- MongoDB
- MoMo
A complete travel platform spanning a web app and a mobile app, letting people search and book tours, hotels, flights and car rentals, and share their trips.
The problem#
Most booking apps treat each service as its own silo — tours in one place, hotels in
another. The goal here was to collapse them into a single booking flow where every
service type is just a variant of one Booking model.
Architecture#
Three independently deployed pieces sharing one data layer:
| Piece | Stack | Responsibility |
|---|---|---|
| Web | ReactJS + Vite | Booking UI, order management |
| Mobile | React Native | On-phone experience, same API |
| Backend | Node.js + MongoDB | REST API, auth, payment handling |
One shared data model#
Rather than four collections for tours, hotels, flights and cars, there is a single
bookings collection discriminated by type, with service-specific data in payload:
type BookingType = 'tour' | 'hotel' | 'flight' | 'car';
interface Booking<T extends BookingType = BookingType> {
_id: ObjectId;
userId: ObjectId;
type: T;
status: 'pending' | 'paid' | 'cancelled';
total: number;
/* Whatever this particular service type needs. */
payload: BookingPayloadMap[T];
createdAt: Date;
}The "My bookings" screen then needs one query instead of four, and adding a new service type never touches the presentation layer.
MoMo payment integration#
The hard part was the webhook. MoMo can deliver the same callback more than once, and makes no ordering guarantee relative to the user's redirect back to the site.
router.post('/momo/ipn', async (req, res) => {
const { orderId, resultCode, signature } = req.body;
if (!verifySignature(req.body, signature)) return res.status(400).end();
/* Idempotent: a duplicate callback cannot overwrite a settled status. */
const booking = await Booking.findOneAndUpdate(
{ _id: orderId, status: 'pending' },
{ status: resultCode === 0 ? 'paid' : 'cancelled' },
{ new: true },
);
/* Always 204 so MoMo stops retrying, even if we had already handled it. */
return res.status(204).end();
});Outcome#
- One booking flow covering four different service types
- Web and mobile share the entire API layer with no duplicated business logic
- Idempotent payments that tolerate duplicate and out-of-order callbacks