While building Chef Claude, a small React application where users enter the ingredients they have and receive an AI-generated recipe, I started wondering how this would actually work.
Should I store recipes somewhere in my code? Or use a recipe API?
Since every user could enter a different combination of ingredients, I couldn't rely on a fixed list of recipes.
That's when I thought of integrating AI into the app.
What I Was Building
The flow of Chef Claude was simple.
Users add ingredients based on what they have available. Every ingredient is stored in state, and after adding at least four ingredients, the "Get a Recipe" button appears.
When the user clicks the button, the ingredient list is sent to the AI model. The model uses those ingredients to generate a recipe and sends the response back.
But the response comes back as Markdown.
Displaying that raw response directly in the UI didn't look good, so I used react-markdown to render the Markdown properly in my React app.
Getting a Hugging Face Access Token
Before connecting my app to an AI model, I needed a Hugging Face access token.
Creating one only took a few minutes:
- Create a Hugging Face account.
- Open Access Tokens.
- Create a new fine-grained token.
- Enable inference permissions.
- Copy the token.
Setting Up the Environment Variable
After getting my Hugging Face token, I needed to use it inside my React application.
Instead of hardcoding my API key directly into my React code, I stored it in a .env file to keep my configuration separate from the source code.
VITE_HF_ACCESS_TOKEN=YOUR_API_KEY
Then I accessed it inside my application using:
const token = import.meta.env.VITE_HF_ACCESS_TOKEN;
At first, I thought keeping my token in a .env file meant it was completely safe.
But after learning how Vite handles environment variables, I realized that wasn't true.
Since my React application needs the token to make the API request, Vite exposes VITE_ environment variables to the frontend during the build process. That also means the browser has access to them.
One thing that confused me was this: if my .env file wasn't part of my application code, then how was the browser still getting my API key?
After understanding how Vite builds the application, it finally clicked. The .env file helps keep my configuration separate from the source code, but VITE_ variables are still injected into the frontend because the browser needs them to make the API request.
That's when I realized my API key wasn't actually secret anymore.
While researching this, I also understood why backend applications are important. Instead of letting the browser call the Hugging Face API directly, the better approach is to keep the API key on the backend. The frontend sends the user's ingredients to the backend, the backend talks to the AI using the secret API key, and only the generated recipe is returned to the frontend.
Even though I haven't learned backend development yet, this was the moment I finally understood why developers keep saying that secret API keys should never live in the frontend.
One thing I learned: Keeping
.envout of your source code and keeping an API key secret are two different things. If the frontend needs the key, the browser will eventually have access to it. That's why sensitive API keys should be kept on a backend or serverless function.
Connecting the Model
After setting up the API key, the next step was connecting my React application to an AI model.
import { HfInference } from "@huggingface/inference";
const SYSTEM_PROMPT = `
You are an assistant that receives a list of ingredients that a user has and suggests a recipe they could make with some or all of those ingredients. You don't need to use every ingredient they mention in your recipe. The recipe can include additional ingredients they didn't mention, but try not to include too many extra ingredients. Format your response in markdown to make it easier to render to a web page
`;
const hf = new HfInference(import.meta.env.VITE_HF_ACCESS_TOKEN);
export async function getRecipeFromMistral(ingredientsArr) {
const ingredientsString = ingredientsArr.join(", ");
const response = await hf.chatCompletion({
// I'm using Qwen 2.5 7B Instruct, but you can replace it with any compatible Hugging Face model.
model: "Qwen/Qwen2.5-7B-Instruct",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{
role: "user",
content: `I have ${ingredientsString}. Please give me a recipe I'd recommend!`,
},
],
});
return response.choices[0].message.content;
}
Let's break down what each part is doing.
HfInference
Instead of manually sending HTTP requests to the Hugging Face API, the SDK provides a client that makes interacting with AI models much simpler.
SYSTEM_PROMPT
The system prompt tells the AI how it should behave.
In my project, I wanted the AI to act like a recipe assistant. It should take the user's ingredients, suggest a recipe, and return the response in Markdown so I could render it nicely in my UI.
Without a system prompt, the AI still responds, but it doesn't have clear instructions about the format or the type of answer I expect.
ingredientsArr.join(", ")
The ingredients entered by the user are stored as an array.
["Rice", "Egg", "Onion", "Tomato"]
Before sending them to the AI, I converted them into a single string using .join(", ").
Rice, Egg, Onion, Tomato
This creates a natural sentence that becomes part of the user's prompt.
chatCompletion()
This is the function provided by the Hugging Face SDK that sends my prompt to the selected AI model and returns its response.
In simple words, this is where my React application actually talks to the AI.
Returning the recipe
Finally, I return:
response.choices[0].message.content
This gives me the generated recipe, which I later render in my React application using react-markdown.
The Response Was Markdown
After connecting the AI model, I thought the hard part was over.
The user clicked the "Get a Recipe" button, the AI generated a recipe, and everything worked.
But there was one small problem.
The response didn't look good in the UI.
The AI was already returning a well-structured Markdown response, and when I rendered it directly, it looked like plain text instead of a nicely formatted recipe.
That's when I discovered react-markdown.
Instead of manually formatting the response, I used react-markdown to render the Markdown as React elements.
The difference was immediately noticeable. Headings, bullet points, and formatting started looking like a proper recipe instead of a block of text.
That small library made the final output look much better without adding much extra code.
Final Thoughts
When I started this project, I thought integrating AI simply meant sending a prompt and getting a response.
By the end, I realized there was much more involved—structuring prompts, handling responses, understanding how environment variables work, and presenting the AI's output in a way that feels polished to the user.
This project also reminded me that every library solves a specific problem. Instead of building everything from scratch, knowing when to use the right tool is just as important.
If you've built something similar or have suggestions, I'd love to hear them in the comments.





Top comments (0)