Build a Next.js Interactive Web-Based Recipe Finder

Written by

in

In today’s fast-paced world, finding the perfect recipe can be a challenge. We’ve all been there – staring into the fridge, wondering what to cook, and then spending ages searching through endless websites. Wouldn’t it be great to have a simple, interactive tool that allows you to quickly find recipes based on ingredients you have on hand? That’s what we’re building today: a web-based recipe finder using Next.js. This tutorial will guide you through creating a user-friendly application, perfect for beginners and intermediate developers looking to hone their skills.

Why Next.js?

Next.js is a powerful React framework that offers several advantages for web development:

  • Server-Side Rendering (SSR) and Static Site Generation (SSG): Next.js allows you to render your application on the server or generate static pages at build time, improving SEO and performance.
  • Fast Refresh: Next.js provides a fast, reliable, and seamless development experience with hot module replacement.
  • Built-in Routing: Next.js simplifies routing with its file-system based router.
  • API Routes: Easily create API endpoints within your Next.js application.
  • Optimized Image Component: The next/image component optimizes images automatically.

Project Overview: Recipe Finder

Our recipe finder will have the following features:

  • Ingredient Input: A user can enter a list of ingredients.
  • Recipe Search: The application fetches recipes from a public API (we’ll use Spoonacular for this example).
  • Recipe Display: The application displays a list of recipes that match the user’s ingredients.
  • User-Friendly Interface: A clean and intuitive design for ease of use.

Prerequisites

Before we begin, make sure you have the following installed:

  • Node.js and npm: You’ll need Node.js and npm (Node Package Manager) installed on your machine. You can download them from nodejs.org.
  • A Code Editor: A code editor like Visual Studio Code, Sublime Text, or Atom.
  • Basic Understanding of JavaScript and React: Familiarity with JavaScript and React concepts will be helpful.

Step-by-Step Guide

1. Setting Up Your Next.js Project

Let’s start by creating a new Next.js project. Open your terminal and run the following command:

npx create-next-app recipe-finder
cd recipe-finder

This command creates a new Next.js project named “recipe-finder” and navigates you into the project directory.

2. Installing Dependencies

For this project, we’ll need to install the following dependencies:

  • axios: For making API requests to fetch recipes.

Run the following command to install these dependencies:

npm install axios

3. Project Structure

Your project directory should look something like this:

recipe-finder/
├── node_modules/
├── pages/
│   └── _app.js
│   └── index.js
├── public/
├── .gitignore
├── next.config.js
├── package-lock.json
├── package.json
└── README.md

The pages directory is where we’ll create our routes. The index.js file inside the pages directory will be the homepage of our application.

4. Creating the Ingredient Input Component

Let’s create a reusable component for the ingredient input. Create a new directory called components in the root directory of your project. Inside the components directory, create a file named IngredientInput.js. This component will handle the input field and manage the list of ingredients.

Here’s the code for IngredientInput.js:

import React, { useState } from 'react';

const IngredientInput = ({ onIngredientsChange }) => {
  const [ingredient, setIngredient] = useState('');
  const [ingredients, setIngredients] = useState([]);

  const handleInputChange = (e) => {
    setIngredient(e.target.value);
  };

  const handleAddIngredient = () => {
    if (ingredient.trim() !== '') {
      setIngredients([...ingredients, ingredient.trim()]);
      setIngredient('');
      onIngredientsChange([...ingredients, ingredient.trim()]); // Pass the updated ingredients
    }
  };

  const handleRemoveIngredient = (index) => {
    const newIngredients = [...ingredients];
    newIngredients.splice(index, 1);
    setIngredients(newIngredients);
    onIngredientsChange(newIngredients); // Pass the updated ingredients
  };

  return (
    <div>
      <input
        type="text"
        value={ingredient}
        onChange={handleInputChange}
        placeholder="Enter ingredient"
      />
      <button onClick={handleAddIngredient}>Add</button>
      <div>
        <ul>
          {ingredients.map((ing, index) => (
            <li key={index}>
              {ing}
              <button onClick={() => handleRemoveIngredient(index)}>Remove</button>
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
};

export default IngredientInput;

This component uses the useState hook to manage the input value and the list of ingredients. It includes an input field, an “Add” button, and a display of the entered ingredients with a “Remove” button for each.

5. Fetching Recipes from the API

We’ll use the Spoonacular API to fetch recipes based on the ingredients entered by the user. You’ll need to sign up for a free API key at Spoonacular.

Create a new file named api.js in the root directory to handle API calls:

import axios from 'axios';

const API_KEY = 'YOUR_API_KEY'; // Replace with your actual API key
const API_URL = 'https://api.spoonacular.com/recipes/findByIngredients';

export const fetchRecipes = async (ingredients) => {
  try {
    const response = await axios.get(API_URL, {
      params: {
        ingredients: ingredients.join(','),
        apiKey: API_KEY,
        number: 10, // Number of recipes to retrieve
      },
    });
    return response.data;
  } catch (error) {
    console.error('Error fetching recipes:', error);
    throw error; // Re-throw the error to be handled by the component
  }
};

Replace 'YOUR_API_KEY' with your actual Spoonacular API key. The fetchRecipes function takes an array of ingredients as input, makes a GET request to the Spoonacular API, and returns the recipe data.

6. Creating the Recipe Display Component

Now, let’s create a component to display the recipes. Create a new file called RecipeList.js in the components directory.

import React from 'react';

const RecipeList = ({ recipes }) => {
  if (!recipes || recipes.length === 0) {
    return <p>No recipes found. Please try different ingredients.</p>;
  }

  return (
    <div>
      <h3>Recipes:</h3>
      <ul>
        {recipes.map((recipe) => (
          <li key={recipe.id}>
            <img src={recipe.image} alt={recipe.title} width="100" />
            <h4>{recipe.title}</h4>
            <p>Missing ingredients: {recipe.missedIngredients.map(ing => ing.name).join(', ')}</p>
          </li>
        ))}
      </ul>
    </div>
  );
};

export default RecipeList;

This component takes an array of recipes as a prop and displays them. It includes a check to handle the case where no recipes are found.

7. Building the Main Page (index.js)

Let’s put it all together in the pages/index.js file:

import React, { useState } from 'react';
import IngredientInput from '../components/IngredientInput';
import RecipeList from '../components/RecipeList';
import { fetchRecipes } from '../api';

const Home = () => {
  const [ingredients, setIngredients] = useState([]);
  const [recipes, setRecipes] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const handleIngredientsChange = (newIngredients) => {
    setIngredients(newIngredients);
  };

  const handleSearch = async () => {
    setLoading(true);
    setError(null);
    setRecipes(null); // Clear previous recipes
    try {
      const data = await fetchRecipes(ingredients);
      setRecipes(data);
    } catch (err) {
      setError(err.message || 'An error occurred while fetching recipes.');
    }
    setLoading(false);
  };

  return (
    <div style={{ margin: '20px' }}>
      <h2>Recipe Finder</h2>
      <IngredientInput onIngredientsChange={handleIngredientsChange} />
      <button onClick={handleSearch} disabled={ingredients.length === 0 || loading}>
        {loading ? 'Searching...' : 'Search Recipes'}
      </button>
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {recipes && <RecipeList recipes={recipes} />}
    </div>
  );
};

export default Home;

This is the main page component. It imports the IngredientInput and RecipeList components, manages the state for ingredients, recipes, loading, and errors, and handles the search functionality. The handleSearch function calls the fetchRecipes function from api.js and updates the state with the results.

8. Styling (Optional)

You can add styling using CSS modules, styled-components, or any other styling method you prefer. For simplicity, we’ll use inline styles in this example. You can create a file called styles.css in the root directory and import it into your components, or you can add styles directly in the components.

/* styles.css */
input {
  padding: 8px;
  margin-right: 10px;
  border: 1px solid #ccc;
  border-radius: 4px;
}

button {
  padding: 8px 15px;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #cccccc;
  cursor: not-allowed;
}

ul {
  list-style: none;
  padding: 0;
}

li {
  margin-bottom: 10px;
  padding: 10px;
  border: 1px solid #eee;
  border-radius: 4px;
}

Import the styles in your index.js file:

import styles from '../styles.css'; // Import the stylesheet

And apply the styles to your components:

<div style={{ margin: '20px' }}>
  <h2 style={{ marginBottom: '20px' }}>Recipe Finder</h2>
  <IngredientInput onIngredientsChange={handleIngredientsChange} />
  <button onClick={handleSearch} disabled={ingredients.length === 0 || loading} style={{ marginTop: '10px' }}>
    {loading ? 'Searching...' : 'Search Recipes'}
  </button>
  {error && <p style={{ color: 'red', marginTop: '10px' }}>{error}</p>}
  {recipes && <RecipeList recipes={recipes} />}
</div>

9. Running Your Application

To run your application, execute the following command in your terminal:

npm run dev

This will start the development server, and you can view your application in your browser at http://localhost:3000.

Common Mistakes and How to Fix Them

1. API Key Errors

Mistake: Forgetting to include your API key or using an incorrect one. The API calls will fail, and you won’t receive any recipe data.

Solution: Double-check that you’ve replaced 'YOUR_API_KEY' in the api.js file with your actual API key from Spoonacular. Also, ensure that your API key is valid and hasn’t expired.

2. CORS Issues

Mistake: You might encounter CORS (Cross-Origin Resource Sharing) errors if the API doesn’t allow requests from your domain. This can prevent your frontend from communicating with the API.

Solution: CORS issues are usually handled on the server-side. However, for development, you can try using a proxy. Create a file called next.config.js in your project’s root directory and add the following code:

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  async rewrites() {
    return [
      {
        source: '/api/recipes',
        destination: 'https://api.spoonacular.com/recipes/findByIngredients?apiKey=YOUR_API_KEY&ingredients=:ingredients&number=10',
      },
    ];
  },
};

module.exports = nextConfig;

Replace 'YOUR_API_KEY' with your actual API key. Then, in your api.js file, change the API_URL to /api/recipes. This sets up a rewrite rule in Next.js to proxy requests to the Spoonacular API. Note this is a basic solution and may need adjustments depending on your API’s requirements and the production environment.

3. Incorrect Ingredient Formatting

Mistake: The API might require a specific format for the ingredients (e.g., comma-separated). If the ingredients are not formatted correctly, the API may not return the expected results.

Solution: Ensure that you’re joining the ingredients array with commas when making the API request. In the api.js file, make sure the ingredients are joined correctly:

ingredients: ingredients.join(','),

4. Handling API Errors Gracefully

Mistake: Not handling API errors properly. If the API request fails (e.g., due to network issues or API errors), your application might crash or display an unhelpful error message.

Solution: Implement proper error handling in your api.js and index.js files. In api.js, use a try…catch block to catch potential errors during the API call, log the error, and re-throw it. In index.js, use a state variable to display an error message to the user.

Key Takeaways

  • Component-Based Architecture: We’ve created reusable components for the ingredient input and recipe display, making the code modular and easier to maintain.
  • API Integration: We’ve learned how to fetch data from an external API using axios.
  • State Management: We’ve used the useState hook to manage component state effectively.
  • Error Handling: We’ve implemented basic error handling to make the application more robust.
  • User Experience: We’ve created a simple and intuitive user interface.

FAQ

1. Can I use a different API?

Yes, you can. You can modify the API_URL and the parameters passed to the API to use a different recipe API. Just make sure to adjust the data parsing in the RecipeList component to match the API’s response format.

2. How can I improve the UI/UX?

You can enhance the UI/UX by:

  • Adding more styling using CSS, CSS modules, or a CSS-in-JS library like Styled Components.
  • Implementing search suggestions or auto-complete for ingredient input.
  • Displaying more recipe details, such as cooking time, ingredients, and instructions.
  • Adding pagination to handle a large number of recipes.

3. How can I deploy this application?

You can deploy your Next.js application to platforms like Vercel (recommended), Netlify, or other hosting providers. Vercel provides seamless integration with Next.js and simplifies the deployment process.

4. What if I exceed the API’s rate limits?

If you exceed the API’s rate limits, you might receive errors. To avoid this, consider implementing the following:

  • Caching: Cache the API responses to reduce the number of requests.
  • Rate Limiting: Implement rate limiting on your server-side (if applicable) to control the number of requests.
  • API Key Management: Consider using a serverless function to make API requests from the backend to protect your API key.

5. How can I add more features?

You can extend this recipe finder by adding features like:

  • User authentication.
  • Saving favorite recipes.
  • Filtering recipes by dietary restrictions.
  • Integrating with a shopping list feature.

Building a recipe finder with Next.js is a fantastic project to learn and practice web development skills. By creating this application, you’ve gained experience with component creation, API integration, state management, and error handling. As you continue to explore Next.js, you’ll find it to be a powerful and versatile framework for building modern web applications. The flexibility of Next.js allows you to expand the functionality and adapt the interface to suit your needs, providing a practical and enjoyable way to learn and grow as a developer. This project serves not just as a functional tool, but as a solid foundation for more complex and ambitious web endeavors.