A modern, full-stack recipe management application. Search for recipes, view details, and manage favourites with a beautiful React frontend and a robust Node.js/Express backend powered by PostgreSQL and the Spoonacular API.
- Frontend-Live-Demo: https://food-recipe-spoonacular.netlify.app/
- Backend-Live-Demo: https://recipe-app-glt5.onrender.com
- Project Overview
- Project Structure
- Features & Functionality
- Environment Variables (.env)
- Setup & Usage
- API Endpoints
- Frontend Walkthrough
- Backend Walkthrough
- Reusing & Extending
- Tech Stack & Keywords
- Conclusion
- Happy Coding! 🎉
This project is a full-stack web app for discovering and saving recipes. It consists of:
- Frontend: React + TypeScript (Vite), modern UI, responsive, easy to extend.
- Backend: Node.js + Express + TypeScript, REST API, PostgreSQL (via Prisma), integrates with Spoonacular API.
Users can search for recipes, view summaries, and manage their favourites. The app is modular, well-documented, and ready for learning or extension.
recipe-app/
├── recipe-app-backend/ # Backend (Node.js, Express, Prisma, PostgreSQL)
│ ├── .env
│ ├── package.json
│ ├── prisma/
│ │ └── schema.prisma
│ └── src/
│ ├── index.ts
│ └── recipe-api.ts
├── recipe-app-frontend/ # Frontend (React, Vite, TypeScript)
│ ├── .env
│ ├── package.json
│ ├── public/
│ │ ├── hero-image.webp
│ │ └── vite.svg
│ └── src/
│ ├── App.tsx
│ ├── App.css
│ ├── api.ts
│ ├── main.tsx
│ ├── types.ts
│ └── components/
│ ├── RecipeCard.tsx
│ └── RecipeModal.tsx
└── README.md # (You are here)
- Recipe Search: Search recipes by keyword (Spoonacular API)
- Recipe Details: View recipe summary in a modal
- Favourites: Add/remove recipes to your favourites, view in a separate tab
- Responsive UI: Works on desktop and mobile
- TypeScript: Type safety throughout
- API Integration: Connects frontend and backend seamlessly
- Modular Code: Easy to extend and reuse
API_KEY=your_spoonacular_api_key
DATABASE_URL=your_postgresql_connection_string
- API_KEY: Get from Spoonacular
- DATABASE_URL: Get from your PostgreSQL provider (Render, Supabase, Neon, Railway, etc.)
VITE_API_URL=http://localhost:5005
- VITE_API_URL: The base URL for your backend API. Use your deployed backend URL in production.
cd recipe-app-backend
npm install
# Configure .env as above
npx prisma migrate dev --name init
npm start
API will be available at: http://localhost:5005/api/recipes/...
cd recipe-app-frontend
npm install
# Configure .env as above
npm run dev
Open http://localhost:5173 in your browser.
GET /api/recipes/search?searchTerm=<term>&page=<page>
— Search recipesGET /api/recipes/:recipeId/summary
— Get recipe summaryPOST /api/recipes/favourite
— Add favourite{ "recipeId": 12345 }
GET /api/recipes/favourite
— List favouritesDELETE /api/recipes/favourite
— Remove favourite{ "recipeId": 12345 }
- Manages state, tabs, search, favourites, and modal.
- Handles API calls via
api.ts
.
const handleSearchSubmit = async (event: FormEvent) => {
event.preventDefault();
setApiError("");
try {
const response = await api.searchRecipes(searchTerm, 1);
if (response.status === "failure" || response.code === 402) {
setApiError(response.message || "API quota reached.");
setRecipes([]);
} else {
setRecipes(Array.isArray(response.results) ? response.results : []);
pageNumber.current = 1;
}
} catch (e) {
setRecipes([]);
}
};
- Handles all API requests to the backend.
export const addFavouriteRecipe = async (recipe: Recipe) => {
const url = new URL("/api/recipes/favourite", API_URL);
const body = { recipeId: recipe.id };
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
};
- RecipeCard: Displays a recipe card with image, title, and favourite button.
- RecipeModal: Shows a modal with the recipe summary and title.
<RecipeCard
key={recipe.id}
recipe={recipe}
onClick={() => setSelectedRecipe(recipe)}
onFavouriteButtonClick={
isFavourite ? removeFavouriteRecipe : addFavouriteRecipe
}
isFavourite={isFavourite}
/>
- Sets up Express, CORS, and JSON parsing.
- Defines all API routes for search, summary, and favourites.
- Handles all external API requests.
- Functions:
searchRecipes
,getRecipeSummary
,getFavouriteRecipesByIDs
.
model FavouriteRecipes {
id Int @id @default(autoincrement())
recipeId Int @unique
}
- Frontend: Components and API functions are modular and reusable. Add new features by following the existing structure.
- Backend: Add new endpoints or models in a RESTful, modular way. Use Prisma for DB migrations.
export const getRecipeInstructions = async (recipeId: string) => {
const url = new URL(`/api/recipes/${recipeId}/instructions`, API_URL);
const response = await fetch(url);
if (!response.ok) throw new Error("Failed to fetch instructions");
return response.json();
};
app.get("/api/recipes/:id/instructions", async (req, res) => {
// Call Spoonacular for instructions
});
- React
- TypeScript
- Vite
- Node.js
- Express
- Prisma
- PostgreSQL
- Spoonacular API
- REST API
- Modular Components
- Hooks
- Responsive Design
- API Integration
- Reusable UI
This project is a modern, extensible, and user-friendly full-stack recipe app. It demonstrates best practices in React, TypeScript, API integration, backend design, and UI/UX. Use it as a learning resource, a starter for your own projects, or as a foundation for your next app.
Feel free to use this project repository and extend this project further!
If you have any questions or want to share your work, reach out via GitHub or my portfolio at https://arnob-mahmud.vercel.app/.
Enjoy building and learning! 🚀
Thank you! 😊