How I Built a Recipe Recommendation Website Using (Mostly) Free Resources

Prit Shah
8 min readApr 12, 2024

--

Login screen of the website

Introduction

I’ve been delving deeper into full stack development lately, still with a particular focus on backend development. This exploration lead me to brainstorm ideas for projects that don’t require heavy frontend development to get started but still tackle a real-world issue in some way. As someone who often struggles to come up with recipe ideas based on the ingredients I have, I wanted to create a platform that offers users a solution to this common dilemma. While the website is still in its early stages, I’m excited to share the progress I’ve made so far. You can find a link to the website and the code below.

Website, Code

Tech Stack

When embarking on this project, I aimed to leverage as many free resources as possible, including for deployment. Here’s the rundown of the tools and technologies I used:

Backend:

  • PostgreSQL (Database management)
  • Java, Spring Boot (REST API development for accessing database / accessing ML model API)
  • Python, Scikit-Learn, Pandas (Machine Learning model development)
  • Flask (REST API development for accessing ML model)
  • Docker, PSQL (Local PostgreSQL testing)
  • Postman (Local API testing)

Frontend:

  • Next.js (Frontend development)
  • Tailwind CSS (Styling)

Authentication:

  • Firebase Authentication (User sign in management)

Deployment:

  • AWS (Hosting the Spring Boot / Flask REST APIs)
  • Porkbun (Buying a custom domain for the website)
  • Vercel (Hosting the Next.js frontend)
  • Supabase (Hosting the PostgreSQL database)

While most resources were entirely free, I did invest some money in a custom domain for the website. The resources I made use of from AWS were also free but only for 12 months within creating an account.

Architecture

The architecture of the entire stack involves the following:

  • Frontend deployed on Vercel
  • ALB (Application Load Balancer) in front of EC2 instance for Spring REST API
  • EC2 (Elastic Compute) instance for Flask REST API
  • EBS (Elastic Block Store) volumes attached to each EC2 instance
  • Auto scaling groups for each EC2 instance
  • PostgreSQL db deployed on Supabase

I’ll delve deeper into the deployment process towards the end of the article.

Database Schema

The database schema encompasses the user, recipe, ingredient, and rating data, which were setup as Entities in Spring to work with the database.

An entity in Spring is a representation of a database table, with each row being an object of the entity. These require a single object to be the primary key, so for anything that requires a composite key — I had to create a separate class object for the composite primary key to be used in that entity. This is why some of the tables in the schema above have primary keys such as RecipeIngredientId for the recipe_ingredients table.

Docker

Docker is a very powerful containerization platform that allows for building lightweight, portable, and self-sufficient application environments. Everything needed to run the application is isolated in the container.

Since I only used it for setting up a local PostgreSQL database for testing purposes, I had a pretty simple Dockerfile:

FROM postgres:latest

# Set environment variables
ENV POSTGRES_USER=admin
ENV POSTGRES_PASSWORD=pwd
ENV POSTGRES_DB=table_example

# Copy SQL initialization script
COPY create_tables.sql /docker-entrypoint-initdb.d/

# To start postgresql db in docker
# docker build -t my-postgres .
# docker run -d -p 5432:5432 my-postgres

# Viewing db in terminal
# psql -h localhost -p 5432 -U admin table_example
# enter in password

What this does is initializes a table named “table_example” and then copies a create_tables.sql file that’s in the same directory locally as the Dockerfile, as the entry point for initializing the database tables. This happens when you run

docker build -t my-postgres .

inside of the directory containing your Dockerfile. Once the build is done, you can run the next command to get your docker container up and running:

docker run -d -p 5432:5432 my-postgres

-d allows you to detach the process from the terminal, so that you can reuse the terminal for other purposes. -p chooses the port you want to run on.

You can then directly access the database in the terminal with PSQL and the environment variables set in the Dockerfile:

psql -h localhost -p 5432 -U admin table_example

Spring Boot Backend

Once I had the database schema setup, I focused on the implementation of the entities and developing CRUD (Create, Read, Update, Delete) operations for each of them. The structure in which I setup the REST APIs involved the following flow:

This allowed me to create endpoints that could work with the frontend without knowing how the data is manipulated in the service logic. The service logic could be changed to interact with the database however was necessary without any changes to the frontend. Spring was also useful in adding authentication to every API call through the Spring filter chain, which is a series of filters that are processed before any incoming request gets routed to the correct controller.

Since I worked with Firebase Authentication, I added a filter that specifically checks if the JWT (Javascript Web Token) token in the request is a valid Firebase generated JWT token. In addition to this, I added another filter for CORS (Cross Origin Resource Sharing) to enable requests from my frontend domain to be processed. By default, a browser ensures that a request to a server is safe through CORS, so in my backend server I needed to specifically handle CORS to authorize the requests.

CORS begins with the browser first sending an OPTIONS request to the server to get back the headers that provide the browser with whether or not it has permission to send the actual API request. The server then sends the appropriate domains and allowed methods in the response headers.

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter extends OncePerRequestFilter {


@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

response.setHeader("Access-Control-Allow-Origin", "YOUR_ALLOWED_DOMAINS");
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "authorization, content-type, xsrf-token");
if ("OPTIONS".equals(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
filterChain.doFilter(request, response);
}

}
}

Next.js Frontend

Using Next.js, which is a Javascript framework built on top of React, was a fun experience. It’s powerful in that it extends React by providing server-side rendering and routing based on page file names, among other features.

Initial development began with the login/sign up functionality, for which I researched using AWS Cognito, a free-tier eligible AWS managed service for user identity and access management. However, this involved using an extra service called AWS Amplify to setup the hosted UI for authorization, where I ran into AWS account level errors that blocked me from setting it up. Switching gears after spending some time trying to solve the issue, I discovered another free to use service: Firebase Authentication.

Firebase Authentication felt very straightforward and intuitive to get up and running for my application. I also added in a component to ensure that a certain page wasn’t accessible unless the user was authenticated. I wrapped all of my pages in this component, except for the login and signup pages.

Once authentication was up and running, I focused on developing user flows to interact with the database and present data on the website. Ensuring data is properly formatted for the user and for REST API calls was a challenge itself. In the Spring logs, when I had any REST API call error, the only error log I was getting was that the request body was missing. This made debugging initially tough, but I was able to eventually resolve the issues.

The styling process involved iterating from basic HTML/CSS to enhancing aesthetics with Tailwind CSS. Despite my limited UI design expertise, I found Tailwind CSS remarkably user-friendly for crafting a simple yet sleek layout.

ML Recommendation Model

My aim with the recommendation model was to provide users with recipe suggestions based on their list of ingredients, aiding them in utilizing available ingredients efficiently. This process entails utilizing a TF-IDF vectorizer on a recipe dataset sourced from Food.com, coupled with computing the cosine similarity between the input ingredients and those of each recipe. The top five recipes with the highest cosine similarity scores are then presented to the user.

TF-IDF, which stands for term frequency-inverse document frequency, plays a pivotal role in this process by converting text documents into numerical vectors based on calculated TF-IDF values. These values are determined by assessing how frequently a term appears in a document and its uniqueness across a collection of documents. In this case, each recipe’s ingredients were the text documents, and each ingredient was a term.

However, there are inherent limitations to this approach. Presently, the same list of ingredients yields identical recipe recommendations, and there’s no personalization based on recipe ratings. To address these constraints, I aim to incorporate more advanced algorithms and additional user metadata in the future, thereby enhancing the recommendation process.

Deploying All of the Pieces

Deployment presented varying levels of complexity across different components of the project. Setting up the PostgreSQL database on Supabase was a smooth and intuitive process, aided by the utilization of Spring entities to establish the initial tables. Similarly, deploying the Next.js website to Vercel proved straightforward; by connecting a GitHub repository to Vercel, updates could be seamlessly pushed, triggering automatic rebuilding and redeployment of the website.

However, deploying the Spring and Flask servers posed challenges. To deploy the Spring REST API, I opted for AWS Beanstalk, which automates environment creation and resource management. Initially intending to use a single EC2 instance with no load balancing for hosting, I encountered complications due to Vercel’s automatic HTTPS deployment conflicting with Beanstalk’s HTTP endpoint. To address this, I reconfigured the Beanstalk environment using an ALB, simplifying HTTPS usage. To setup HTTPS on the Beanstalk endpoint I followed the steps below:

  • Bought a custom domain
  • Created an ACM (AWS Certificate Manager) certificate for the custom domain or subdomain (I used api.culinarycompanion.xyz for the API endpoint)
  • Associated the certificate with the ALB and enabled HTTPS (port 443) traffic
  • Allowed HTTPS traffic on the EC2 instance
  • Created a CNAME record for the custom endpoint to target the ALB DNS name (ALB IPs can change, so you have to use the DNS name)

I initially attempted to deploy the flask server to Railway, where I ran into issues due to having a large CSV file as part of the deployment. This ultimately lead me to using Beanstalk as well for the Flask deployment, which involved packaging the Flask app with all the needed files in a zip. This worked on Beanstalk, since the zip file size limit here is 500 mb. For the production environment, I used Gunicorn to serve the Flask app. Flask comes with a built-in server, but it is not recommended for production use due to potential security vulnerabilities and limited capabilities.

Conclusion

Creating a full-stack website is a substantial endeavor, considering all the components involved, yet it’s an immensely gratifying journey. I’ve gained invaluable insights throughout this process and am eager to enhance the website further while expanding my knowledge in full-stack development. Thank you for taking the time to read about my project!

--

--

Prit Shah

Software Engineer and CS Masters Student at Georgia Tech