Built end to end — solo
Trippz
A travel-booking REST API — users search and book flights, hotels and custom trips; providers and agencies manage offerings; admins run the platform. Around 80 documented endpoints across 13 modules, with live interactive Swagger docs.
01The problem
Sign-in has to work through email/password and three social providers (Google, Facebook, Apple), and the resulting session has to be stateless so the API can scale horizontally and stay simple to reason about — without leaving long-lived tokens that can't be rotated.
02The constraint
Three OAuth providers plus local auth all have to resolve to one user identity and one authorization model; the API is stateless (no server session store); and it still needs Stripe payments with refunds, Twilio / Resend notifications, uploads and rate limiting around that core.
03What I built
A layered Express + TypeScript API (controller → service → Prisma → PostgreSQL) with ~80 endpoints across 13 modules — auth, users, destinations, hotels, flights, trips, bookings, payments, reviews, notifications, providers, agencies and admin.
Auth unifies local login and Google / Facebook / Apple OAuth into one user record, then issues a short-lived JWT access token (15 min) and a longer refresh token (7 days) so the API holds no session state.
Bookings run over Stripe with refunds; Twilio (SMS) and Resend (email) handle notifications; Cloudinary handles uploads; requests are rate-limited and validated with consistent JSON error shapes.
The whole surface is documented as live, interactive Swagger/OpenAPI, and the project ships with Jest / Supertest tests, Docker and a GitHub Actions pipeline.
04The decision
Options considered
Auth had to span local login and three OAuth providers while staying stateless enough to scale horizontally.
- AServer sessions in a store — easy to revoke, but stateful, so every node needs the store and it becomes a scaling dependency.
- BA single long-lived JWT — stateless and simple, but you can't revoke it before it expires.
- CA short-lived access JWT plus a rotating refresh token — stateless requests, with revocation handled at refresh time.
Chose Short-lived access token plus rotating refresh token.
The trade-off I accepted
Server sessions make revocation trivial but tie every request to a shared session store — a stateful dependency I didn't want on a horizontally-scaled API. A single long-lived JWT is stateless but effectively un-revocable until it expires. I split the difference: a 15-minute access token keeps requests stateless and fast to verify, and a 7-day refresh token carries the ability to rotate or revoke. The cost is that a stolen access token stays valid for up to its 15 minutes — I accepted that window in exchange for a stateless request path and revocation that lives at the refresh boundary, and kept the window small on purpose.
05The result
- ~80 endpoints across 13 modules, all live behind interactive Swagger docs.
- Local plus Google, Facebook and Apple sign-in resolve to one identity and one JWT access/refresh model (15 min / 7 days).
- Stripe payments with refunds, Twilio + Resend notifications, Cloudinary uploads, rate limiting and Jest/Supertest coverage around the core.
06Stack
- Express
- TypeScript
- Prisma
- PostgreSQL
- JWT + OAuth
- Stripe
- Twilio
- Resend
- Cloudinary
- Swagger