A full-stack hotel search application for ski resorts, split into two packages:
client/β React + TypeScript + Vite frontendserver/β Node.js/TypeScript backend (Express REST API)
ski-resorts/
βββ client/ # React + TypeScript + Vite frontend
β βββ src/
β βββ public/
β βββ package.json
β βββ vite.config.ts
βββ server/ # Express + TypeScript backend
βββ src/
β βββ server.ts # Express server & initialization
β βββ index.ts # Module entry point
β βββ types/ # TypeScript interfaces
β βββ providers/ # ExternalApiProvider.ts (AWS Lambda integration)
β βββ managers/ # HotelSearchManager.ts (core search logic)
β βββ controllers/ # HotelSearchController.ts (request handlers)
β βββ routes/ # hotelRoutes.ts (API routes)
β βββ middleware/ # validation.ts (validation & error handling)
β βββ config/ # configuration
β βββ utils/ # helper functions
β βββ data/ # hotels.json (ski resorts reference)
βββ dist/ # compiled JavaScript (after build)
βββ package.json
βββ tsconfig.json
cd server
npm install
npm run build
npm run dev # http://localhost:9000cd client
npm install
npm run devThe backend is a production-ready service for the hotel search web application. It features a scalable architecture that supports multiple API providers with intelligent aggregation, multi-group size requests, and comprehensive validation.
- Multi-Provider Architecture: Easy to add new API providers
- Smart Multi-Group Aggregation: Automatically fetches larger room sizes for more options
- Parallel Execution: All requests run simultaneously for optimal performance
- Result Deduplication: Removes duplicates based on hotel name + capacity + price
- Price Sorting: Results sorted by price in ascending order
- Comprehensive Validation: Input validation with detailed error messages
- TypeScript for type safety
- Express.js REST API
- Modular architecture (MVC pattern)
- Middleware-based validation
- Error handling and logging
- Configuration management
POST /api/hotels/search
Request:
{
"query": {
"ski_site": 1, // 1-5 (ski resort ID)
"from_date": "03/04/2025", // DD/MM/YYYY format
"to_date": "03/11/2025", // DD/MM/YYYY format
"group_size": 4 // 1-10 people
}
}Response:
{
"success": true,
"data": {
"hotels": [
{
"id": "hotel-123",
"name": "Hotel Name",
"room_type": "Standard",
"capacity": 4,
"price": 150,
"available_rooms": 2,
"ski_site": 1,
"provider": "ExternalHotelsSimulator"
}
],
"totalCount": 25,
"query": { /* original query */ }
}
}Test it with curl:
curl -X POST http://localhost:9000/api/hotels/search \
-H "Content-Type: application/json" \
-d '{
"query": {
"ski_site": 1,
"from_date": "03/04/2025",
"to_date": "03/11/2025",
"group_size": 2
}
}'1. Provider Pattern
// Add new providers by implementing the interface
interface ApiProvider {
name: string;
search(query: SearchQuery): Promise<Hotel[]>;
}2. Multi-Group Aggregation Flow
User searches for group_size: 2
β
Manager calculates: [2, 3, 4, 5, 6, 7, 8, 9, 10]
β
Sends 9 parallel requests to external API
β
Aggregates all results
β
Deduplicates entries
β
Sorts by price
β
Returns combined results
3. Error Handling
- Invalid input β 400 Bad Request
- Server error β 500 Internal Server Error
- Provider failure β Graceful recovery (returns results from other providers)
| Field | Rules |
|---|---|
ski_site |
Required, must be positive number |
from_date |
Required, format: DD/MM/YYYY |
to_date |
Required, format: DD/MM/YYYY, must be after from_date |
group_size |
Required, must be 1-10 |
- Parallel Requests: ~9x faster than sequential
- Set-Based Deduplication: O(n) time complexity
- Price Sorting: O(n log n) time complexity
- Error Resilience: One provider failure doesn't crash search
1: Val Thorens
2: Courchevel
3: Tignes
4: La Plagne
5: Chamonix
The React frontend should:
- Send requests to
POST /api/hotels/search - Parse response with structure:
{ success, data: { hotels, totalCount, query } } - Handle errors: check response status and error field
- Display results: map over hotels array to render UI
Example:
async function searchHotels(query) {
const response = await fetch('/api/hotels/search', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query })
});
if (!response.ok) {
const error = await response.json();
console.error('Search failed:', error.error);
return;
}
const data = await response.json();
displayHotels(data.data.hotels);
}- β Input validation on all fields
- β Type-safe TypeScript implementation
- β CORS configured
- β Error messages don't expose system details
- β Middleware-based protection
npm run build
PORT=8080 npm run startEnvironment Variables
PORT=8080 # Server port
NODE_ENV=production # Environment
DEBUG=false # Debug loggingnpm run dev # Start development server with hot reload
npm run build # Compile TypeScript to JavaScript
npm run start # Run compiled JavaScript (production)- Streaming/progressive responses
- Result caching with TTL
- Rate limiting
- Advanced filtering (amenities, ratings)
- Pagination
- Analytics and monitoring
The client is built on Vite's minimal React + TypeScript template, providing HMR and preconfigured ESLint rules.
Two official Vite plugins are available for Fast Refresh:
- @vitejs/plugin-react β uses Babel
- @vitejs/plugin-react-swc β uses SWC
If you are developing a production application, it's recommended to enable type-aware lint rules:
- Configure the top-level
parserOptionsproperty:
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})- Replace
tseslint.configs.recommendedwithtseslint.configs.recommendedTypeCheckedortseslint.configs.strictTypeChecked - Optionally add
...tseslint.configs.stylisticTypeChecked - Install eslint-plugin-react and update the config:
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})- Extensible: Add new backend providers without touching existing code
- Performant: Parallel execution for speed
- Reliable: Graceful error handling and recovery
- Maintainable: Clean architecture, TypeScript for safety throughout client and server
- Production-Ready: Includes validation, error handling, and logging