Build a Next.js Interactive Web-Based Recipe Search App

Written by

in

Tired of endlessly scrolling through recipe websites, only to find the same old dishes? Imagine a world where you could instantly search for recipes based on ingredients you have on hand, dietary restrictions, or even cooking time. In this tutorial, we’ll build a fully functional, interactive recipe search application using Next.js, a powerful React framework, and the Spoonacular API, a vast database of recipes. This project is perfect for both beginners and intermediate developers looking to expand their skillset and create something useful.

Why Build a Recipe Search App?

Beyond the convenience of finding delicious meals, building this app offers several learning opportunities:

  • API Integration: You’ll learn how to fetch data from an external API, parse it, and display it in your application.
  • Dynamic Routing: We’ll implement dynamic routes to display individual recipe details.
  • User Input and State Management: You’ll handle user input, manage application state, and update the UI accordingly.
  • UI Design with CSS Modules: We’ll learn how to style our components using CSS Modules for a clean and maintainable codebase.

This project provides a practical way to learn these essential web development concepts while creating something fun and practical.

Prerequisites

Before we begin, make sure you have the following:

  • Node.js and npm (or yarn) installed: These are essential for running JavaScript applications and managing dependencies.
  • A basic understanding of HTML, CSS, and JavaScript: Familiarity with these languages is crucial for understanding the code.
  • A code editor: VS Code, Sublime Text, or any other editor of your choice.
  • A Spoonacular API Key: You’ll need to sign up for a free API key at Spoonacular. This key allows you to access their recipe data. Keep this key safe; do not share it.

Setting Up the 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-search-app

This command creates a new directory called recipe-search-app with the basic structure of a Next.js project. Navigate into the project directory:

cd recipe-search-app

Next, install the necessary dependencies. We’ll need axios for making API requests:

npm install axios

Or, if you prefer yarn:

yarn add axios

Project Structure

Before diving into the code, let’s establish a clear project structure. This will help us organize our components and make our application easier to maintain. We’ll use the following structure:

recipe-search-app/
 ├── pages/
 │   ├── _app.js
 │   ├── index.js
 │   └── [recipeId].js
 ├── components/
 │   ├── RecipeCard.js
 │   ├── RecipeDetails.js
 │   └── SearchForm.js
 ├── styles/
 │   ├── globals.css
 │   └── RecipeCard.module.css
 ├── .gitignore
 ├── next.config.js
 ├── package.json
 └── README.md

Here’s a breakdown of the key files and directories:

  • pages/: This directory contains the pages of our application.
  • pages/index.js: The home page, where users will search for recipes.
  • pages/[recipeId].js: A dynamic route for displaying recipe details.
  • components/: This directory will hold our reusable React components.
  • components/RecipeCard.js: Displays a brief overview of each recipe.
  • components/RecipeDetails.js: Displays the detailed information of a selected recipe.
  • components/SearchForm.js: Handles the recipe search input and submission.
  • styles/: Contains our CSS modules.
  • styles/globals.css: Contains global CSS styles.

Building the Search Form

Let’s start by creating the search form component. This component will handle user input and trigger the recipe search. Create a file named SearchForm.js inside the components directory.

Here’s the code for SearchForm.js:

import React, { useState } from 'react';

const SearchForm = ({ onSearch }) => {
  const [query, setQuery] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    onSearch(query);
  };

  return (
    <form onSubmit={handleSubmit} style={{ marginBottom: '20px' }}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search for recipes..."
        style={{ padding: '10px', marginRight: '10px', borderRadius: '5px', border: '1px solid #ccc', width: '300px' }}
      />
      <button type="submit" style={{ padding: '10px', borderRadius: '5px', backgroundColor: '#4CAF50', color: 'white', border: 'none', cursor: 'pointer' }}>Search</button>
    </form>
  );
};

export default SearchForm;

In this code:

  • We import the useState hook to manage the search query.
  • The query state variable stores the user’s input.
  • The handleSubmit function prevents the default form submission behavior and calls the onSearch prop function with the search query. This onSearch function will be passed from the parent component (index.js).
  • The form includes an input field for the search query and a submit button.

Creating Recipe Cards

Next, let’s create the RecipeCard component. This component will display a brief summary of each recipe, including its title, image, and a link to view more details. Create a file named RecipeCard.js inside the components directory.

Here’s the code for RecipeCard.js:

import React from 'react';
import Link from 'next/link';
import styles from '../styles/RecipeCard.module.css';

const RecipeCard = ({ recipe }) => {
  return (
    <div className={styles.card}>
      <img src={recipe.image} alt={recipe.title} className={styles.image} />
      <h3 className={styles.title}>{recipe.title}</h3>
      <Link href={`/${recipe.id}`}>
        <a className={styles.link}>View Recipe</a>
      </Link>
    </div>
  );
};

export default RecipeCard;

And create RecipeCard.module.css inside the styles directory with the following content:

.card {
  border: 1px solid #ccc;
  border-radius: 8px;
  padding: 16px;
  margin-bottom: 16px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  width: 300px;
  text-align: center;
}

.image {
  width: 100%;
  height: 200px;
  object-fit: cover;
  border-radius: 8px;
  margin-bottom: 8px;
}

.title {
  font-size: 1.2rem;
  margin-bottom: 8px;
}

.link {
  display: inline-block;
  padding: 8px 16px;
  background-color: #4CAF50;
  color: white;
  text-decoration: none;
  border-radius: 4px;
  transition: background-color 0.2s ease;
}

.link:hover {
  background-color: #3e8e41;
}

In this code:

  • We import Link from next/link to create a navigation link to the recipe details page.
  • We import the CSS module for styling.
  • The component receives a recipe prop, which contains the recipe data.
  • The component displays the recipe’s image, title, and a link to the details page.
  • The link uses the dynamic route /${recipe.id} to navigate to the recipe details page.

Fetching Recipes from the API

Now, let’s fetch recipes from the Spoonacular API. We’ll do this in the index.js file (the home page).

Here’s the code for pages/index.js:

import React, { useState } from 'react';
import axios from 'axios';
import SearchForm from '../components/SearchForm';
import RecipeCard from '../components/RecipeCard';

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

  const apiKey = process.env.NEXT_PUBLIC_SPOONACULAR_API_KEY; // Accessing the API key from environment variables

  const handleSearch = async (query) => {
    setLoading(true);
    setError(null);
    setRecipes([]); // Clear previous results

    try {
      const response = await axios.get(
        `https://api.spoonacular.com/recipes/complexSearch?apiKey=${apiKey}&query=${query}&number=10`
      );

      if (response.data.results) {
        setRecipes(response.data.results);
      } else {
        setError('No recipes found.');
      }
    } catch (err) {
      setError('An error occurred while fetching recipes.');
      console.error(err);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div style={{ padding: '20px' }}>
      <h1>Recipe Search</h1>
      <SearchForm onSearch={handleSearch} />

      {loading && <p>Loading...</p>}
      {error && <p style={{ color: 'red' }}>{error}</p>}

      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '20px' }}>
        {recipes.map((recipe) => (
          <RecipeCard key={recipe.id} recipe={recipe} />
        ))}
      </div>
    </div>
  );
};

export default Home;

In this code:

  • We import useState, axios, SearchForm, and RecipeCard.
  • We define state variables for recipes, loading, and error.
  • We declare the API key using process.env.NEXT_PUBLIC_SPOONACULAR_API_KEY. This assumes you’ve set the API key as an environment variable (see the “Setting up Environment Variables” section below).
  • The handleSearch function is called when the user submits the search form.
  • Inside handleSearch, we use axios.get to make a request to the Spoonacular API. The URL includes the API key and the search query.
  • If the API call is successful, we update the recipes state with the fetched data.
  • We handle loading and error states to provide feedback to the user.
  • We map through the recipes array and render a RecipeCard for each recipe.

Setting Up Environment Variables

It’s crucial to store your API key securely and avoid hardcoding it directly into your code. Next.js provides a convenient way to manage environment variables.

Create a .env.local file in the root of your project. This file is where you’ll store your environment variables. Add the following line, replacing YOUR_API_KEY with your actual Spoonacular API key:

NEXT_PUBLIC_SPOONACULAR_API_KEY=YOUR_API_KEY

The NEXT_PUBLIC_ prefix is important. It makes the environment variable accessible in your client-side code. Without this prefix, the variable will only be available on the server-side.

Creating Recipe Details Page

Now, let’s create the recipe details page, which will display the full details of a selected recipe. Create a file named [recipeId].js inside the pages directory. This file uses the bracketed syntax ([recipeId].js) to create a dynamic route that accepts a recipe ID as a parameter.

Here’s the code for pages/[recipeId].js:

import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import axios from 'axios';

const RecipeDetails = () => {
  const router = useRouter();
  const { recipeId } = router.query;
  const [recipe, setRecipe] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  const apiKey = process.env.NEXT_PUBLIC_SPOONACULAR_API_KEY;

  useEffect(() => {
    const fetchRecipeDetails = async () => {
      if (!recipeId) return;
      setLoading(true);
      setError(null);
      try {
        const response = await axios.get(
          `https://api.spoonacular.com/recipes/${recipeId}/information?apiKey=${apiKey}`
        );
        setRecipe(response.data);
      } catch (err) {
        setError('An error occurred while fetching recipe details.');
        console.error(err);
      } finally {
        setLoading(false);
      }
    };

    fetchRecipeDetails();
  }, [recipeId]);

  if (loading) return <p>Loading...</p>;
  if (error) return <p style={{ color: 'red' }}>{error}</p>;
  if (!recipe) return <p>Recipe not found.</p>;

  return (
    <div style={{ padding: '20px' }}>
      <h1>{recipe.title}</h1>
      <img src={recipe.image} alt={recipe.title} style={{ width: '100%', maxWidth: '500px', marginBottom: '20px' }} />
      <p><b>Servings:</b> {recipe.servings}</p>
      <p><b>Ready in:</b> {recipe.readyInMinutes} minutes</p>
      <h2>Ingredients:</h2>
      <ul>
        {recipe.extendedIngredients.map((ingredient) => (
          <li key={ingredient.id}>{ingredient.original}</li>
        ))}
      </ul>
      <h2>Instructions:</h2>
      <div dangerouslySetInnerHTML={{ __html: recipe.instructions }} />
    </div>
  );
};

export default RecipeDetails;

In this code:

  • We import useRouter from next/router to access the route parameters.
  • We use the recipeId parameter from the route to fetch the recipe details from the Spoonacular API.
  • We use the useEffect hook to fetch the recipe details when the component mounts and whenever the recipeId changes.
  • We handle loading and error states.
  • We display the recipe’s title, image, servings, ready time, ingredients, and instructions. The instructions are displayed using dangerouslySetInnerHTML because they are often provided as HTML from the API. Be cautious when using this; ensure the data source is trusted to avoid potential security vulnerabilities (e.g., cross-site scripting attacks).

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them:

  • API Key Issues: Double-check your API key in the .env.local file and ensure it’s correctly formatted and that you’ve included the `NEXT_PUBLIC_` prefix. Also, confirm the key is active and hasn’t exceeded any rate limits.
  • CORS Errors: If you encounter CORS (Cross-Origin Resource Sharing) errors, it means your browser is blocking requests to the Spoonacular API. This is less likely with Next.js, as the API requests are made server-side. However, if you’re experiencing this, make sure your API key is correctly configured.
  • Incorrect API Endpoint: Verify that you’re using the correct API endpoints from the Spoonacular API documentation. Typos or incorrect parameters can lead to errors.
  • Missing Dependencies: Ensure you’ve installed all the necessary dependencies (e.g., axios). Run npm install or yarn install in your project directory to install any missing dependencies.
  • Incorrect File Paths: Double-check the file paths for your components and CSS modules.
  • State Management Issues: If your data isn’t updating correctly, review your state management logic. Make sure you’re correctly updating state variables using the setRecipes, setLoading, and setError functions.

Running the Application

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

npm run dev

Or, if you prefer yarn:

yarn dev

This command starts the development server. Open your browser and go to http://localhost:3000 to view the application.

Key Takeaways

  • Next.js Fundamentals: You’ve learned about setting up a Next.js project, creating components, handling user input, making API calls, and managing state.
  • API Integration: You’ve successfully integrated with an external API to fetch and display data.
  • Dynamic Routing: You’ve implemented dynamic routes to create a details page for each recipe.
  • CSS Modules: You’ve used CSS Modules for styling, promoting a clean and maintainable codebase.
  • Environment Variables: You’ve learned how to securely store and access API keys using environment variables.

SEO Best Practices

To ensure your recipe search app ranks well in search engines, consider the following SEO best practices:

  • Descriptive Titles: Use clear and concise titles for your pages. The title should include relevant keywords (e.g., “Recipe Search App”).
  • Meta Descriptions: Write compelling meta descriptions for each page, summarizing the content and including relevant keywords.
  • Header Tags: Use header tags (<h1>, <h2>, etc.) to structure your content and highlight important keywords.
  • Image Optimization: Optimize your images for web use. Use descriptive alt text for your images.
  • Internal Linking: Link between your pages to improve navigation and SEO.
  • Mobile-Friendly Design: Ensure your app is responsive and works well on all devices.
  • Keyword Research: Research relevant keywords that users might search for and incorporate them naturally into your content.

FAQ

  1. Can I use a different API? Yes, you can. The core concepts of this tutorial apply to any API. You would need to adjust the API endpoints and the data structure parsing accordingly.
  2. How can I add pagination to the search results? You can add pagination by modifying the API request to include parameters for offset or page number. Then, you’ll need to add pagination controls (e.g., “Next” and “Previous” buttons) to your UI.
  3. How can I add more search filters? You can extend the search form to include additional input fields for filters like dietary restrictions (e.g., vegetarian, vegan, gluten-free), cuisine types, or cooking time. You would then modify the API request to include these filter parameters.
  4. How do I deploy this application? You can deploy your Next.js application to platforms like Vercel, Netlify, or AWS. These platforms provide simple deployment processes.
  5. Why use CSS Modules? CSS Modules help to avoid naming conflicts by scoping CSS styles locally to a component. This makes it easier to manage and maintain your styles, especially in larger projects.

You’ve successfully built a functional recipe search application using Next.js! By following this tutorial, you’ve gained practical experience with essential web development concepts, from API integration and dynamic routing to state management and UI design. You now have a solid foundation for building more complex and interactive web applications using Next.js. Remember to experiment with different features, explore the Spoonacular API further, and continue to refine your skills. The journey of a thousand lines of code begins with a single search query, and this project is a fantastic step on that journey. Congratulations on completing this tutorial, and happy coding!