Skip to content

Spec: Hotel Discovery API #1

Description

@kernvex

Spec: Hotel Discovery API

A production-ready RESTful API for finding and exploring hotels, backed by the provided 40-Property dataset. Vocabulary follows CONTEXT.md; decisions respect docs/adr/0001–0003.

Problem Statement

A traveller browsing a travel platform needs to find Properties that fit their trip and quickly tell which ones are actually bookable for their dates — without wading through irrelevant Properties or discovering only late that nothing is open for their Stay window. From the platform's side, there is no backend service yet that lets a client search the Property catalogue, drill into a single Property's full details, and check live Room availability and pricing for a set of travel dates.

Solution

A lightweight RESTful API over the Property catalogue that lets a client:

  • Search Properties, filtering by city, star rating, and price.
  • Retrieve a single Property's full details by id.
  • Check availability — see which Rooms in a Property are open for a given Stay window, with per-night and total pricing.

The catalogue is loaded in memory from the provided dataset behind a repository interface. The service is self-documenting (OpenAPI/Swagger) and ships with a health check, tests, CI, and a container image.

User Stories

  1. As an API client, I want to list all Properties, so that I can browse the full catalogue.
  2. As a traveller, I want to filter Properties by city, so that I only see options where I'm travelling.
  3. As a traveller, I want city filtering to be case-insensitive, so that "chicago" and "Chicago" both match.
  4. As a traveller, I want city matching to be exact (not substring), so that "York" does not wrongly match "New York".
  5. As a traveller, I want to filter Properties by an exact star rating, so that I can target a specific class of hotel.
  6. As a traveller, I want to filter Properties by a minimum price, so that I can exclude options below a floor.
  7. As a traveller, I want to filter Properties by a maximum price, so that I can stay within budget.
  8. As a traveller, I want to use minimum and maximum price independently or together, so that "under $200" or "$100–$200" both work.
  9. As a traveller, I want price filtering to use each Property's cheapest Room (price_from), so that "from $X" matches how I think about hotel prices.
  10. As a traveller, I want to combine city, star, and price filters, so that all supplied criteria narrow the results together (AND).
  11. As an API client, I want the Property list to return a compact summary per Property (id, name, city, country, star_rating, overall_rating, review_count, price_from), so that I can render result cards without over-fetching.
  12. As an API client, I want a search with no matches to return an empty successful result, so that "nothing found" is not treated as an error.
  13. As an API client, I want to retrieve a single Property's full details by id, so that I can show its description, address, contact, amenities, policies, ratings, and Rooms.
  14. As an API client, I want a request for an unknown Property id to return Not Found, so that I can distinguish missing resources from other failures.
  15. As a traveller, I want to check Room availability for a Property by supplying check-in and check-out dates, so that I can see what is bookable for my trip.
  16. As a traveller, I want only the Rooms that are available for my whole Stay window returned, so that I never see options I cannot book.
  17. As a traveller, I want availability to require every Night of my Stay window (check-in up to but not including check-out), so that a two-night stay correctly needs both of its Nights.
  18. As a traveller, I want each available Room to show its price per night, so that I can compare nightly rates.
  19. As a traveller, I want each available Room to show the total price and the number of nights for my Stay window, so that I know the full cost, not just the nightly rate.
  20. As a traveller, I want a Property with no open Rooms for my dates to return an empty successful result, so that "no availability" is a clear, non-error outcome.
  21. As an API client, I want check-in and check-out to be required on the availability endpoint, so that a missing date is rejected rather than silently ignored.
  22. As an API client, I want a check-out on or before check-in to be rejected as a bad request, so that nonsensical Stay windows fail fast.
  23. As an API client, I want malformed dates, out-of-range star ratings, and a minimum price above the maximum to be rejected as bad requests, so that invalid input is caught at the boundary.
  24. As an API client, I want all errors in one consistent shape ({ error: { code, message } }), so that I can handle failures uniformly.
  25. As an operator, I want a health-check endpoint, so that a load balancer or orchestrator can probe liveness.
  26. As a developer, I want interactive API documentation (Swagger UI), so that I can explore and exercise every endpoint without writing a client.
  27. As a reviewer, I want the availability, filtering, and pricing logic covered by exhaustive unit tests, so that I can trust the trickiest behaviour.
  28. As a reviewer, I want each endpoint covered by integration tests that also assert the response matches its published schema, so that the contract is provably enforced.
  29. As a maintainer, I want the data source hidden behind a repository interface with a documented migration path, so that swapping the in-memory store for Postgres/SQLite touches one place.

Implementation Decisions

  • Stack: TypeScript + Node + Fastify; input validation with Zod via the Fastify Zod type-provider (one schema → runtime validation + inferred types); npm; Node 20 LTS pinned.
  • Layering: routes → services → repository (interface) plus pure domain functions (isRoomAvailable, filterHotels, priceFrom) with no framework or I/O dependency. Services depend on the HotelRepository interface, never on the seed directly.
  • Data source (ADR-0001): the dataset is loaded into memory at startup behind HotelRepository. No database. A migration guide to Postgres/SQLite is documented.
  • Availability (ADR-0002): a Room is available for a Stay window iff every Night in the half-open interval [check_in, check_out) is present in that Room's open dates (per-Night set membership). Checkout day is not required. Empty open-dates → never available.
  • Price (ADR-0003): price_from = the minimum price_per_night across a Property's Rooms; price filtering uses it and is independent of date availability.
  • Endpoints:
    • GET /hotels — optional filters city (case-insensitive exact), star_rating (exact integer 1–5), min_price, max_price (over price_from), combined with AND. Returns a summary projection wrapped as { data: [...] }. No pagination. No matches → 200 { data: [] }.
    • GET /hotels/:id — full Property object; unknown id → 404.
    • GET /hotels/:id/rooms?check_in&check_out — dates required; returns available Rooms (each with price_per_night, total_price, nights) wrapped as { data: [...] } with the Stay window echoed as metadata; unknown id → 404; none available → 200 { data: [] }.
    • GET /health200.
  • Errors: only 400 (invalid/malformed input; missing required dates; check_out <= check_in; star_rating out of range; min_price > max_price) and 404 (unknown Property). Empty results are 200, never 404. A central error handler renders all failures — including normalized Zod validation errors — as { error: { code, message } }.
  • Wire format: snake_case throughout (query params and response bodies), matching the brief and the dataset. Derived fields (price_from, total_price, nights) follow the same convention.
  • Response envelope: list responses are wrapped { data: [...] } (keeps future pagination non-breaking); single-resource detail is returned directly.
  • Self-documentation: OpenAPI + Swagger UI at /docs, generated from the Zod request/response schemas.

Testing Decisions

  • What a good test is: it exercises external behaviour (inputs → outputs, status codes, response shape), never implementation details. Tests must survive a refactor that preserves behaviour.
  • Framework: Vitest.
  • Two seams (see the seam discussion in the design thread):
    1. HTTP boundary via app.inject() (primary, highest seam) — exercises routing → validation → services → repository → domain end-to-end, in-process. Covers each endpoint's full contract: happy path, 404, 400, and empty 200. Each response is asserted to conform to its Zod response schema (schema-conformance / contract enforcement).
    2. Pure domain functions (secondary, deliberately lower seam) — the availability/filter/pricing edge-case matrix: nights-vs-days, one-night stays, empty open-dates, the sparse 2026-07-14 upper boundary, a synthetic gapped open-dates set (proving per-Night membership rather than range containment), AND filter combinations, and price_from across multi-Room Properties.
  • Modules tested: the pure domain functions (exhaustive) and every endpoint (integration). Depth over blanket coverage percentage.
  • Prior art: none — greenfield. This spec establishes the two-seam pattern for the repo.
  • Contract tests: enforced via Zod response-schema conformance in the integration tests. Consumer-driven contract tests (Pact) are deliberately not used — there is no consumer service — and this is documented.

Out of Scope

  • A real database or any persistence (in-memory only; migration path documented, not built).
  • Pagination (deliberately omitted at 40 records; documented, and the { data } envelope keeps it non-breaking).
  • Authentication and authorization (no 401/403).
  • Amenity filtering, region/country filtering, guest-count/occupancy filtering, and sorting.
  • Multi-currency (USD assumed; the dataset has no currency field).
  • Booking, reservations, holds, or any mutation of inventory — this is a read-only discovery API.
  • Consumer-driven contract testing (Pact).
  • The frontend exercise (the backend option was chosen).

Further Notes

  • Dataset characteristics to respect: availability is per-Night not per-day; the open dates all fall 2026-07-10..14 and are treated as the source of truth rather than gated on the real current date; exactly 15% of Properties (6 of 40) have no availability at all; "free Wi-Fi" is the one amenity that is not clean snake_case; pricing exists only at the Room level; regions/states are non-US for international Properties.
  • Operational hardening (part of this feature): tsconfig strict + noUncheckedIndexedAccess; ESLint (flat) + Prettier; a Husky pre-push hook running typecheck → lint → test (fail-fast); a GitHub Actions CI workflow mirroring the hook plus a docker build job; a multi-stage Dockerfile (slim base, non-root, .dockerignore, HEALTHCHECK against /health).
  • Documentation deliverables: README.md (quickstart, endpoint reference + Swagger link, architecture, testing, and an AI-usage section) and a standalone ASSUMPTIONS.md capturing every decision and its rationale, including the Pact "know when not to reach for it" note.
  • AI usage is documented transparently per the brief and reflected in the commit history.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an AFK agent

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions