Scoutcamp is my bachelor's thesis (TFG) project that digitized the full cycle of scout camp discovery, booking, and management: find camps, check availability by date, book plots or bungalows, and let owners handle requests from their own dashboard. It replaces spreadsheets, scattered emails, and manual confirmations with a single web flow.
Live demo: scoutcamp.alejandroquintana.dev. Code: frontend and backend.
Domain and actors
The data model revolves around:
- Camping — camp listing (location with
2dsphereindex, images, rules, check-in/out, payment methods, owner). - CampingLodging — accommodation types:
campsite,bungalow, orother, with capacity and nightly fee. - CampingUnit — bookable units within each lodging.
- Booking — reservation with dates, units, manager contact,
total cost, payment method, and status (
pending,accepted,rejected,cancelled). - CampingRelation — favorites and reviews per user/camp.
User roles (simple but effective RBAC):
- user — search camps, book, view bookings, conversations, and profile.
- manager — create/edit camps, manage lodgings/units, accept or reject bookings, messaging.
- admin — global panel: users, camps, bookings, conversations.
The technical challenge
- Contextual search: filter by check-in/out dates,
geolocation (
$geoNear), and real unit availability in that range. - Avoid overselling: validate free units before persisting a booking.
- Rich listings: show camps with lodgings, aggregated capacity, and ratings without naive N+1 queries.
- Owner authorization: only the camp
owneredits listings or changes booking status. - i18n: API
i18n(es, en, fr, de) for emails; frontend@ngx-translate. - Media: image upload to Google Cloud Storage via
Documentmodel and Multer.
System architecture
Angular 18 SPA against a Express 5 REST API
under /api/v1. JWT auth: checkUser decodes the token
on each request; authMiddleware guards routes that require a session.
flowchart TB
subgraph client [Angular 18 SPA]
Public[Listing and booking]
Manager[Manager panel]
Admin[Admin panel]
end
subgraph api [Node Express 5]
Auth[JWT checkUser]
Routes[api/v1 routes]
Ctrl[Controllers and validators]
end
subgraph data [Persistence]
Mongo[(MongoDB Mongoose 9)]
GCS[Google Cloud Storage]
Mail[Nodemailer]
end
Public --> Routes
Manager --> Routes
Admin --> Routes
Routes --> Auth
Routes --> Ctrl
Ctrl --> Mongo
Ctrl --> GCS
Ctrl --> Mail
Tech stack
- Frontend: Angular 18, TypeScript 5.4, Angular Material,
Bootstrap 5, RxJS,
@ngx-translate, Google Maps, JWT with@auth0/angular-jwt. - Backend: Express 5, Mongoose 9, JWT, bcrypt,
express-validator, Multer,
@google-cloud/storage, i18n, Morgan, CORS.
Backend: API and business logic
Main routes mounted in index.js:
/api/v1/— login and signup (auth.routes).-
/api/v1/campings— camp CRUD, lodgings, bookings, reviews (public or authenticated per endpoint). /api/v1/users— profiles and user bookings./api/v1/conversations— user messaging./api/v1/documents— file upload/download on GCS.
All models extend a base databaseSchema with a static
search() method: reusable pagination, sorting, filters, and
populate for admin and user lists.
Booking flow (createBooking)
- Client sends dates, requested lodgings, manager details, and payment method.
-
Server loads available lodgings via
CampingLodging.getAvailableLodgingsand free units viaCampingUnit.getAvailableUnits. - If stock is insufficient for any lodging, respond with error (no save).
-
Compute
totalCost(nights × rate × units) and create booking aspending. -
Email camp owner in their language (
i18n.setLocale+ Nodemailer).
Aggregations in camp listings
Camping.getCampings builds an aggregation pipeline with
$lookup into lodgings, units, and relations; computes average
rating; optionally applies $geoNear when coordinates are provided;
and filters camps with bookings overlapping the searched dates. Public listings
arrive enriched in one database round trip.
Frontend: modules and routes
The Angular app is split by domain:
- camping —
campings-list,camping-view(gallery, map, reviews, booking),camping-booking. - manager — my camps, create/edit camp, lodging management, conversations.
- admin — global users, camps, bookings, conversations.
- auth / user — signup, login, profile, user area.
Shared components: camp searcher, reusable tables, side panel (left-menu), language selector, and navbar.
Challenges solved
1. Availability without overselling
Problem: two users might try to book the last plot for the same dates.
Solution: before booking.save(), ensure each
requested lodging has availables greater than or equal to the
requested quantity; assign only units returned by
getAvailableUnits. On failure, throw Unauthorized
without writing to the database.
2. Listings without naive N+1
Problem: show camps with lodgings, units, and ratings in paginated lists with many chained queries.
Solution: aggregation pipeline in getCampings with
$lookup, $addFields for total capacity and average
reviews, and date-overlap booking filters.
3. Authorization and ownership
Problem: users must not modify others' camps or bookings.
Solution: JWT in Authorization header;
camping.owner.equals(req.user._id) checks on booking management
and camp edits; admin role for broader user listings.
4. Multilingual experience
Problem: owners and users speak different languages, especially in email notifications.
Solution: backend i18n package (es, en, fr, de) for
email subjects and bodies; frontend @ngx-translate with a language
selector in the UI.
5. Images and documents
Problem: each camp needs a reliable photo gallery.
Solution: Multer buffer upload, stream to GCS bucket
(scoutcamp_bucket), metadata in Document collection
referenced from the camp.
Conclusion
Scoutcamp solidified designing a REST API with Mongoose, aggregations for complex queries, and a role-based modular Angular SPA. As a thesis project, it showed that search, booking, owner management, and global admin can live in one coherent product without inflating the model with organizational hierarchies the domain did not need.
Key takeaways: model availability on the server, enrich listings with pipelines instead of manual joins in the client, and clearly separate public, manager, and admin modules on the frontend.