A recipe search and bookmarking app that lets you browse 1M+ meals, adjust serving sizes, and save your favourites — built with vanilla JS and the Forkify API.
⚠️ Desktop only — this app is not optimised for mobile or tablet viewports.
- 🔍 Search 1,000,000+ recipes powered by the Forkify API
- 📖 Detailed recipe view with ingredients, cooking time, and servings
- ⚖️ Dynamic serving adjuster — ingredient quantities scale automatically
- 🔖 Bookmark your favourites — persisted in
localStorage; no account or backend needed - ➕ Upload your own recipes using the built-in form
- 📄 Pagination for clean browsing through large result sets
Forkify is a frontend-only, no-backend application. All state is managed in memory and persisted exclusively through the browser's localStorage — there is no server, database, or authentication layer.
The codebase follows a strict Model–View–Controller separation:
| Layer | Responsibility |
|---|---|
model.js |
Application state, all API calls, localStorage read/write |
views/*.js |
DOM rendering only — zero business logic |
controller.js |
Wires model and views together; owns all event handler logic |
Views expose a addHandler(handler) method (the publisher). The controller passes its own functions as subscribers at startup. This keeps views completely decoupled from the controller — a view never imports or calls the controller directly.
Every view extends a shared View base class that provides:
render(data)— generates markup and inserts it into the DOMupdate(data)— diffs the new markup against the current DOM and patches only changed nodes, avoiding full re-rendersrenderSpinner()/renderError()/renderMessage()— shared UI states
// Example: extending the base View
class RecipeView extends View {
_parentElement = document.querySelector('.recipe');
_generateMarkup() {
return `<div class="recipe__title">${this._data.title}</div> ...`;
}
}While the view layer uses classes, the model and helpers lean functional:
- Pure functions with no side effects wherever possible
Array.prototype.map,filter,reduce, andflatMappreferred over imperative loopsasync/awaitwith a sharedgetJSONhelper for all network requests- State mutations are isolated to a single
stateobject inmodel.js, never scattered across the app
- Node.js v14 or higher
- npm (comes with Node)
# 1. Clone the repository
git clone https://github.com/your-username/forkify.git
cd forkify
# 2. Install dependencies
npm install
# 3. Start the development server
npm startThe app will open at http://localhost:1234 with hot reloading enabled.
npm run buildOutput is placed in the dist/ folder, ready to deploy.
This project uses the Forkify API v2.
Searching works without a key. To upload your own recipes, generate a free key:
- Visit forkify-api.herokuapp.com
- Click Generate API Key
- Open
src/js/config.jsand replace the placeholder:
export const KEY = 'your-api-key-here';forkify/
├── src/
│ ├── img/ # Static images & icons
│ ├── sass/ # SCSS styles
│ └── js/
│ ├── controller.js # App entry point & pub/sub wiring
│ ├── model.js # State, API calls, localStorage persistence
│ ├── config.js # App-wide constants (API URL, key, timeouts)
│ ├── helpers.js # Pure utility functions (getJSON, timeout, etc.)
│ └── views/
│ ├── View.js # Base class (render, update, error states)
│ ├── recipeView.js # Recipe detail rendering
│ ├── searchView.js # Search input handling
│ ├── resultsView.js # Search results list
│ ├── bookmarksView.js # Bookmarks panel
│ ├── paginationView.js # Page controls
│ └── addRecipeView.js # Upload recipe modal
├── index.html
├── package.json
└── README.md
| Concern | Choice |
|---|---|
| Language | Vanilla JavaScript (ES6+) |
| Paradigm | OOP (ES6 classes) + functional style |
| Markup | HTML5 |
| Styling | CSS3 / SASS |
| Bundler | Parcel v2 |
| API | Forkify API v2 (external, read-only for search) |
| Persistence | localStorage (no backend, no database) |
| Architecture | MVC + Publisher–Subscriber |
- Search —
searchViewpublishes the query;controllercallsmodel.loadSearchResults(), then re-rendersresultsViewandpaginationView. - Recipe Detail — Clicking a result updates the URL hash;
controllercallsmodel.loadRecipe()and passes data torecipeView.render(). - Servings —
+/−buttons fire an event; the controller updatesmodel.stateand callsrecipeView.update()to patch only the changed quantity nodes. - Bookmarks — Toggling a bookmark mutates
model.state.bookmarks, persists tolocalStorage, and re-rendersbookmarksView. Bookmarks are restored fromlocalStorageon page load. - Upload — The modal form POSTs to the Forkify API with your key; on success the new recipe is loaded and bookmarked automatically.
- Desktop only — no responsive/mobile layout implemented
- No backend — data is stored in
localStorage; clearing browser storage removes all bookmarks - No user accounts — bookmarks are local to the browser and device
- API dependency — recipe data is sourced entirely from the external Forkify API
Contributions are welcome! Please open an issue first to discuss what you'd like to change.
# Fork → branch → commit → pull request
git checkout -b feature/your-feature-nameThis project is licensed under the MIT License.
- Recipe data provided by the Forkify API
- Project inspired by Jonas Schmedtmann's JavaScript course

