A REST API for searching and browsing hotel inventory. Built with Node.js 22, Fastify 5, and TypeScript.
- Node.js 22 LTS (
lts/jod) — use nvm to manage versions - npm (bundled with Node)
- Docker + Docker Compose (optional, for containerized local dev)
nvm use # picks up .nvmrc → Node 22
npm installcp .env.example .env
# Edit .env if you need non-default values (port, log level, etc.)npm run devThe server starts with hot-reload via tsx watch. Default: http://localhost:3000.
Verify it's up:
curl http://localhost:3000/health
# {"status":"ok","uptime":0.123,"timestamp":"..."}docker compose upThis builds the image using the builder stage (full dev dependencies, tsx watch for hot-reload) and mounts ./src into the container so edits reflect immediately without a rebuild.
The health check polls GET /health every 10 seconds.
To build and run the production image instead:
docker build -t hotel-discovery-api .
docker run -p 3000:3000 -e NODE_ENV=production hotel-discovery-apinpm testUses Node's built-in node:test runner. No third-party test framework.
npm run test:coverageUses c8 (V8 native coverage). Outputs a text summary and an lcov report in coverage/.
npm run typecheckRuns tsc --noEmit — no output files, just compile-time validation.
npm run build # compiles TypeScript → dist/
npm start # runs dist/index.jsThe Helm chart lives in ci/helm/. It supports two environments via value overrides:
# Dev
helm upgrade --install hotel-discovery-api ./ci/helm \
-f ci/helm/values.dev.yaml \
--set image.tag=$(git rev-parse --short HEAD)
# Prod
helm upgrade --install hotel-discovery-api ./ci/helm \
-f ci/helm/values.prod.yaml \
--set image.tag=$(git rev-parse --short HEAD)Update ci/helm/values.dev.yaml and ci/helm/values.prod.yaml with your actual image repository and ingress hostnames before deploying.
| Variable | Default | Description |
|---|---|---|
NODE_ENV |
development |
production disables dev-only behavior |
PORT |
3000 |
Port the HTTP server listens on |
HOST |
0.0.0.0 |
Bind address |
LOG_LEVEL |
info |
Pino log level (debug, info, warn) |
All endpoints under /api/v1 require an Authorization: Bearer <token> header. Token content is not validated in the current build — see ASSUMPTIONS.md for context.
Infrastructure endpoints (/health, /ready) are unauthenticated.
| Method | Path | Description |
|---|---|---|
| GET | /health | Liveness probe — always 200 |
| GET | /ready | Readiness probe — always 200 |
Search and filter hotels. Returns a paginated list of hotel summaries (rooms omitted, room_count included).
Query parameters (all optional):
| Parameter | Type | Description |
|---|---|---|
city |
string | Case-insensitive exact match on city name |
state |
string | Case-insensitive exact match on state / region |
country |
string | Case-insensitive exact match on country name |
star_rating |
integer | Exact star rating (1–5) |
min_rating |
number | Minimum overall_rating (inclusive) |
amenity |
string | Hotel must have this amenity; repeat for AND logic |
min_price |
number | Hotel must have ≥1 room with price_per_night ≥ this value |
max_price |
number | Hotel must have ≥1 room with price_per_night ≤ this value |
check_in |
string | ISO date (YYYY-MM-DD); must be paired with check_out |
check_out |
string | ISO date (YYYY-MM-DD); hotel must have ≥1 room available every night |
limit |
integer | Page size (1–100, default 20) |
offset |
integer | Page offset (default 0) |
Response 200:
{
"data": [
{
"id": "hotel-01",
"name": "The Grand Luminary",
"star_rating": 5,
"overall_rating": 4.8,
"review_count": 1240,
"address": { "city": "Chicago", "state": "IL", "country": "USA", "..." : "..." },
"amenities": ["pool", "free Wi-Fi", "spa"],
"room_count": 2
}
],
"meta": { "total": 40, "limit": 20, "offset": 0 }
}Errors:
400—star_ratingout of 1–5 range, or invalid date range (only one ofcheck_in/check_outprovided, orcheck_in >= check_out)401— missing or malformedAuthorizationheader
Note: Unknown query parameters are silently stripped by Fastify 5 rather than rejected. See ASSUMPTIONS.md for details.
Retrieve full details for a single hotel, including all room definitions.
Response 200: Full hotel object with rooms array.
Errors:
401— missing or malformedAuthorizationheader404— hotel not found
Retrieve available rooms and pricing for a hotel, optionally scoped to a specific stay.
Query parameters (all optional):
| Parameter | Type | Description |
|---|---|---|
check_in |
string | ISO date (YYYY-MM-DD); must be paired with check_out |
check_out |
string | ISO date (YYYY-MM-DD, exclusive — the departure day) |
max_price |
number | Maximum price_per_night |
min_occupancy |
integer | Room must accommodate at least this many guests |
bed_type |
string | Case-insensitive match (King, Queen, Double, Full, Twin, Futon) |
When check_in and check_out are provided:
- Only rooms available for every night of the stay are returned.
- Each room in the response includes a computed
total_pricefield (price_per_night × nights). - The
metablock includescheck_in,check_out, andnights.
Response 200:
{
"data": [
{
"room_id": "room-01a",
"type": "Deluxe King Room",
"bed_type": "King",
"bed_count": 1,
"max_occupancy": 2,
"square_footage": 450,
"price_per_night": 299.00,
"total_price": 598.00,
"room_amenities": ["city_view", "mini_bar"],
"available_dates": ["2026-07-10", "2026-07-11", "2026-07-12"]
}
],
"meta": {
"total": 1,
"check_in": "2026-07-10",
"check_out": "2026-07-12",
"nights": 2
}
}Errors:
400— only one ofcheck_in/check_outprovided, orcheck_in >= check_out401— missing or malformedAuthorizationheader404— hotel not found
Retrieve a single room by ID within a hotel.
Response 200: Full room object including available_dates.
Errors:
401— missing or malformedAuthorizationheader404— hotel or room not found
- Runtime: Node.js 22 LTS (ESM,
"type": "module") - HTTP framework: Fastify 5 — chosen for native TypeScript support, JSON Schema validation, and fast serialization
- Language: TypeScript 5 in strict mode
- Testing: Node built-in
node:test+c8for coverage
src/
config.ts Environment variable loading with fail-fast validation
app.ts Fastify instance factory (plugins, routes)
index.ts Entrypoint — binds the server to the configured port
plugins/
auth.ts Bearer token gate (stub — see ASSUMPTIONS.md)
routes/
health.ts /health and /ready — no auth
hotels.ts All /api/v1/hotels/** routes
index.ts Route registration and prefix wiring
data/
hotels.ts Static hotel dataset (40 properties)
types/
index.ts Shared TypeScript types
test/
health.test.ts
hotels.test.ts
ci/
helm/ Kubernetes Helm chart (dev + prod value sets)
There is no database. All data is served from an in-memory TypeScript array (src/data/hotels.ts) backed by a Map for O(1) ID lookups. See ASSUMPTIONS.md for the reasoning.
All /api/v1/** routes are protected by a verifyToken preHandler. The current implementation checks that an Authorization: Bearer <token> header is present and returns 401 if it is not. Token content is intentionally not validated — the plugin is structured to make plugging in real JWT/JWKS verification a single-file change. See src/plugins/auth.ts and ASSUMPTIONS.md.