StayFinder
1. Introduction
StayFinder is the final project for the courses IDATA2301 Web Technologies and IDATA2306 Application Development at the Norwegian University of Science and Technology (NTNU) in Ålesund.
This project is a hotel price aggregator web application — similar to Trivago or Kayak — that allows users to search for hotels based on their preferences and compare prices across multiple booking sites to find the best deals.
Because this site is meant to be a price aggregator, we have a fake booking-system with no payment processing, meant to simulate the user being sent to a 3rd party booking site.
Please note that since this is a school project, most of the content is fictional. The application does not provide real services or provide accurate information.
This README file pertains to the backend development (IDATA2306). Some content may overlap with the README of the frontend repository, where relevant.
2. Repository Structure
The repository is structured as follows:
Stayfinder/
├── logs/ # Debug and Hibernate logs
│ ├── debug.log
│ └── hibernate.log
├── poastman/ # Postman test collection
│ ├── Full Test Suite.postman_collection.json # Postman collection
│ └── local.postman_environment.json # Environment for tests
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── edu.ntnu.group8.stayfinder/
│ │ │ ├── composite/ # Composite keys
│ │ │ │ └── RoomWebsitePricingId
│ │ │ ├── controller/ # REST API controllers
│ │ │ │ ├── AmenityController
│ │ │ │ └── ...
│ │ │ ├── dto/ # Data Transfer Objects
│ │ │ │ ├── AddFavoriteDTO
│ │ │ │ └── ...
│ │ │ ├── entity/ # JPA entities
│ │ │ │ ├── Entity
│ │ │ │ ├── Hotel
│ │ │ │ └── ...
│ │ │ ├── repository/ # JPA repositories
│ │ │ │ ├──AmenityRepository
│ │ │ │ └── ...
│ │ │ ├── security/ # Security & Authentication
│ │ │ │ ├── SecurityConfig
│ │ │ │ └── ...
│ │ │ ├── service/ # Service classes
│ │ │ │ ├── AmenityService
│ │ │ │ └── ...
│ │ │ └── StayFinderApplication
│ │ └── resources/
│ │ ├── application.properties # Spring Boot configuration
│ │ └── logback-spring.xml # Logging configuration
│ └── test/
├── .gitignore
├── photo-handling.md
├── README.md
├── spring-report.md
├── StayFinder.postman_collection.json # Postman collection
└── pom.xml
3. Deployed Project Architecture
Our project follows a server-side monolithic architecture with REST API communication, where the frontend, backend, and database are all hosted on the same server (provided by the school). The backend application (built with Java and Spring Boot) is cloned and run directly on the server, exposing a REST API. The frontend (HTML, CSS, and JavaScript) is cloned into a separate directory and served via NGINX. The frontend communicates with the backend through HTTP requests to this REST API, and the backend in turn interacts with a locally hosted MySQL database.
This setup reflects a variation of a classic three-tier architecture, with all tiers deployed on a single node. To streamline deployment, we've configured automatic pulling from both the frontend and backend Git repositories on the server.
The architecture can be visualized as follows:
+----------------------+
| NGINX Server |
| (serving frontend) |
+----------+-----------+
|
+------------v-------------+
| Frontend (static) |
| HTML/CSS/JS |
+------------+-------------+
|
v
+------------+-------------+
| Backend (Java) |
| REST API (Spring Boot) |
+------------+-------------+
|
v
+----------+-----------+
| MySQL Database |
+----------------------+
Diagram generated by ChatGPT based on our description of the architecture.
The flow of data is as follows:
Image from Lecture 11 PowerPoint titled "Full-stack monolith" by Di Wu.
4. Security with HTTPS
Our backend is secured with HTTPS via an NGINX reverse proxy. The Spring Boot application itself listens internally on port 8080, while NGINX handles all HTTPS traffic on port 443. This means that all external communication — including API calls from the frontend — is encrypted using HTTPS.
We have not explicitly configured HTTPS within the Spring Boot application (e.g. via application.properties), because the backend is not exposed directly to the internet. Instead, it is protected behind NGINX, which terminates the TLS connection and forwards requests securely to the backend.
This separation of concerns — where NGINX handles TLS and the application focuses on business logic — is a widely-used and recommended approach in production environments.
The diagram below illustrates this setup:
[ User's Browser ]
|
| HTTPS (port 443)
v
[ NGINX Reverse Proxy ]
|
| HTTP (port 8080)
v
[ Spring Boot Backend ]
|
| JDBC (MySQL)
v
[ MySQL Database ]
Diagram generated by ChatGPT based on the text above (written by us).
5. Technologies Used
- Java: The primary programming language used for the backend.
- Spring Boot: A framework for building Java applications, used to create the REST API.
- Spring Data JPA: A part of the Spring framework that simplifies database access and management.
- MySQL: The relational database management system used to store application data.
6. Database
The database is hosted on the server and is managed using MySQL. We have entities for hotels, users, bookings, and amenities, among others, representing tables in the database.
Below is a diagram of the database schema, showing the relationships between the entities (generated by DataGrip):
7. How to Run the Project Locally
7.1 Prerequisites
- You must be connected to the NTNU network (directly or via VPN).
- You need access to a properly configured .env file.
- You need backend access credentials and database configuration (only provided to project developers).
If you have the backend set up, ensure:
-
The .env file in the backend project includes:
DB_USERNAME DB_PASSWORD JWT_SECRET
-
CORS is configured to allow requests from the frontend (add the following to the
securityFilterChain
method inSecurityConfigurer
class):config.addAllowedOrigin("http://localhost:63342"); // Adjust for your local frontend port
7.2 Running the Backend
- Clone the backend repository.
- Open the project in your IDE (e.g., IntelliJ or VSCode).
- Navigate to the
StayFinderApplication
class and run it.
7.3 Important Notes
- All debug or lower logs will appear in
logs/debug.log
. - Hibernate logs will appear in
logs/hibernate.log
.
8. Postman Tests
This project has been tested using Postman.
8.1 Importing the Postman Collection & Environment
- Make sure you have the Postman version with the "Scripts" tab containing "Pre-request" and "Post-response" rather than the "Tests" tab.
- Importing the Postman collection:
- Open Postman and go to the "Collections" tab.
- Click on "Import" and select the
Full Test Suite.postman_collection.json
file from the project directory.
- Importing the environment:
- Go to the "Environments" tab in Postman.
- Click on "Import" and select the
local.postman_environment.json
file from the project directory.
- Set the environment:
- In the top right corner of Postman, select the imported environment from the dropdown menu.
8.2 Running Tests
- Open the imported collection "Full Test Suite" in Postman.
- Right-click on the collection and select "Run".
8.3 Notes
- The tests are organized into folders based on the functionality they test. You can click on each folder for a description of each test within with expected results.
- If you wish to test the backend running locally, you may change the environment variable
{{BASE_URL}}
tohttp://localhost:8080
in the Postman environment settings.
9. Highlighted Features (Extras)
This section highlights some key features that we believe set our project apart (extras). While there are many features implemented throughout the application, we want to explicitly mention the ones that we think make our project unique or interesting, and which could count as an extra outside the requirements.
9.1 Hotel Search & Filtering (Backend)
The backend exposes a single endpoint for hotel search and filtering:
POST /api/hotels/search/filter
This endpoint accepts a JSON body matching the HotelFilterRequest
DTO, which contains all possible filtering and sorting parameters, including:
- Location (hotel name, city, or country)
- Check-in and check-out dates
- Number of guests and rooms
- Selected amenities
- Price range (min and max)
- Sorting preference (e.g., price ascending/descending, star rating)
Note: A POST request is used instead of GET to accommodate complex and potentially lengthy parameter structures. This is a common and accepted practice when working with structured request bodies. (For more information, check out this article: https://www.linkedin.com/pulse/why-we-use-post-instead-get-fetch-data-modern-web-deepika-naik-8mo2c/)
9.1.1 Processing Flow
-
Request Handling
The controller receives theHotelFilterRequest
object from the frontend and passes it to the service layer. -
Validation
The service layer validates date inputs:- Dates must not be in the past.
- Check-out must be after check-in.
-
Database Query Execution
The service delegates the request to theHotelRepository
, which uses a dynamic@Query
to perform all filtering directly at the database level. This ensures efficiency and avoids unnecessary in-memory filtering in Java. -
Result Mapping
The raw query result is a list of custom objects returned from the database, each representing a hotel with relevant fields (e.g., id, name, location, etc.). These are mapped to DTOs in the service layer and returned to the frontend.
9.1.2 Notes
- All filtering happens in the database, not in Java code. This makes the process more scalable and performant.
- The system returns only hotels that match all selected criteria, including matching all selected amenities.
- The endpoint supports flexible, multi-parameter filtering in a single request, making it simple for the frontend to retrieve up-to-date results based on user preferences.
- A fallback "Get All Hotels" endpoint is used in case filtering should fail.
9.2 Popular Hotels
The "Popular Hotels" section displays the top 10 most-clicked hotels across all users. This data is updated dynamically based on user interactions.
How it works:
- When a user clicks on a hotel card, the frontend sends a POST request to the backend containing the
user_id
andhotel_id
. If the user is not logged in, theuser_id
is set tonull
, to keep accurate statistics. - This request is handled by the backend, which records the click in the
viewed_hotels
table, along with the current timestamp (NOW()
). - The frontend fetches the top 10 most-clicked hotels from the backend and displays them in the "Popular Hotels" section on the front page, sorted by the number of clicks in descending order.
9.3 Recently Viewed Hotels
The "Recently Viewed Hotels" section shows the most recent hotels clicked by each individual user. Each user only sees their own recently viewed hotels. If a user is not logged in, they're prompted to log into start tracking their viewed hotels.
How it works:
- When a user clicks a hotel, the frontend sends a POST request to the backend with the
user_id
andhotel_id
, same as in the "Popular Hotels" section. - The backend records the click in the
viewed_hotels
table, including the timestamp (NOW()
). - The frontend fetches a list of hotels viewed by the current user by sending the
user_id
in the GET request, along with an optional parameter for the number of hotels to return (default is 10). The hotels are then returned in order from most to least recently clicked. - This list is displayed under the "Recently Viewed Hotels" section, ensuring that each user sees only the hotels they have recently interacted with.
9.4 Admin Page
We've implemented a feature which lets an admin hide and show hotels. This is the only admin requirement, however we've also decided to extend admin functionality to include a full admin page where the admin can manage amenities (and in the future, all other aspects of the website).
The admin can currently:
- Add Amenity: The admin can add new amenities to the database.
- Edit Amenity: The admin can edit existing amenities (name and description).
- Delete Amenity: The admin can delete amenities from the database.
- Hide/Show Hotels: The admin can hide hotels from the public view, making them invisible to users. They can also choose to make them visible again.
We've also readied buttons for adding, editing, and deleting hotels, seeing all users, managing rooms, etc., but these features are not yet fully implemented, and are meant to showcase what we would include on a real admin page.
9.5 User Profile
The user profile page allows users to view and edit their personal information, including first name, last name, email, and password.
The user can also choose to delete their account. Deleting their account will set
their user_id
to null
in the viewed_hotels
table, and remove all their other data,
including bookings from the database.
10. Image Handling
Refer to the photo-handling.md
file for details on how we handle images in the project.
11. Further Work
11.1 Filtering Query
The current filtering logic in the HotelRepository
is implemented using a single,
complex @Query
. While it functions correctly, the query is long, difficult
to maintain, and not easily extensible. In the future, it would be beneficial
to refactor this into smaller, reusable components—either by using the Criteria
API, QueryDSL, Spring Data Specifications, or something similar. These approaches can help make the
logic more modular, readable, and easier to modify as new filtering features are
added.
11.2 Additional Postman Tests
While we have a comprehensive set of Postman tests, there are still some areas that could benefit from further testing. For example, we could benefit from more negative tests to ensure that the system behaves as expected when invalid data is provided. Additionally, we could add tests for edge cases, such as filtering with no results or testing the limits of the database (e.g., maximum number of hotels, maximum number of amenities). These tests would help ensure that the system is robust and can handle a variety of scenarios without errors.
11.3 Decouple "Recently Viewed Hotels" and "Popular Hotels"
It would be ideal to decouple the "Recently Viewed Hotels" and "Popular Hotels" features,
making them independent of each other. Currently, both features rely on the same
viewed_hotels
table, which can lead to confusion and potential data integrity issues.
However, we figured that for a project of this size, it would be acceptable to keep them
together, as they are both based on the same concept of hotel views.