Build a Simple Next.js Interactive Recipe App

In today’s digital age, the ability to create interactive and dynamic web applications is a highly sought-after skill. Next.js, a powerful React framework, provides developers with the tools to build fast, SEO-friendly, and user-friendly web experiences. This article will guide you through building a simple, yet functional, interactive recipe application using Next.js. This project is perfect for beginners and intermediate developers looking to expand their skillset and understand the core concepts of Next.js.

Why Build a Recipe App?

Recipe applications are an excellent choice for learning web development for several reasons:

  • Data Handling: They involve fetching and displaying data, often from an external source (like a database or a public API), teaching you about data fetching and state management.
  • User Interaction: Recipes often have interactive elements like search, filtering, and favoriting, which allows you to explore event handling and user interface (UI) design.
  • Real-World Relevance: Everyone eats! Building something practical makes the learning process more engaging and provides a tangible project to showcase your skills.

Prerequisites

Before we begin, ensure you have the following installed on your machine:

  • Node.js and npm (or yarn): You’ll need Node.js and npm (Node Package Manager) or yarn to manage project dependencies. You can download them from nodejs.org.
  • A Code Editor: A code editor like Visual Studio Code (VS Code), Sublime Text, or Atom is recommended.
  • Basic Understanding of HTML, CSS, and JavaScript: Familiarity with these languages is essential for understanding the code.

Project Setup

Let’s get started by setting up our Next.js project. Open your terminal or command prompt and run the following command:

npx create-next-app recipe-app

This command will create a new Next.js project named “recipe-app”. Navigate into the project directory:

cd recipe-app

Now, let’s install some dependencies we’ll need for this project. We’ll use a library to fetch data from a public API. For this example, we will not use any external dependencies, but if you wanted to, you could use a library like ‘axios’ or ‘node-fetch’. To install, run:

npm install # or yarn install

Project Structure

Your project directory should look something like this:

recipe-app/
├── node_modules/
├── package.json
├── package-lock.json
├── .gitignore
├── next.config.js
├── pages/
│   ├── _app.js
│   ├── index.js
│   └── api/
│       └── recipes.js
├── public/
│   └── ...
└── README.md
  • pages/: This is where your application’s pages will reside. Each file in this directory represents a route in your application. For example, `pages/index.js` will be your homepage (/).
  • public/: This directory is for static assets like images, fonts, and other files.
  • _app.js: This file is used to initialize pages. You can override this to implement custom configurations.
  • next.config.js: This is where you can configure Next.js settings.

Fetching Recipe Data

To keep things simple, we’ll use a placeholder API to fetch our recipe data. You can find many free recipe APIs online (e.g., Spoonacular API, Recipe Puppy API). For this example, we will use a hardcoded JavaScript array, to represent the data.

Create a file called `recipes.js` inside the `pages/api` directory. This file will contain our recipe data. Replace the existing content with the following (or use your own recipe data if you have an API key):

// pages/api/recipes.js

export default function handler(req, res) {
  const recipes = [
    {
      id: 1,
      name: "Spaghetti Carbonara",
      ingredients: ["Spaghetti", "Eggs", "Pancetta", "Parmesan Cheese", "Black Pepper"],
      instructions: [
        "Cook spaghetti according to package directions.",
        "Fry pancetta until crispy.",
        "Whisk eggs, cheese, and pepper.",
        "Combine pasta, pancetta, and egg mixture. Toss quickly.",
        "Serve immediately."
      ],
      image: "/spaghetti-carbonara.jpg", // Add an image URL
      prepTime: "15 minutes",
      cookTime: "20 minutes",
      servings: 2
    },
    {
      id: 2,
      name: "Chicken Stir-Fry",
      ingredients: ["Chicken", "Broccoli", "Soy Sauce", "Ginger", "Garlic", "Rice"],
      instructions: [
        "Cut chicken into bite-sized pieces.",
        "Stir-fry chicken with vegetables.",
        "Add soy sauce, ginger, and garlic.",
        "Serve over rice."
      ],
      image: "/chicken-stir-fry.jpg", // Add an image URL
      prepTime: "20 minutes",
      cookTime: "15 minutes",
      servings: 3
    },
    {
      id: 3,
      name: "Chocolate Chip Cookies",
      ingredients: ["Flour", "Butter", "Sugar", "Eggs", "Chocolate Chips"],
      instructions: [
        "Preheat oven to 375°F (190°C).",
        "Cream butter and sugar.",
        "Add eggs and mix.",
        "Stir in flour and chocolate chips.",
        "Bake for 10-12 minutes."
      ],
      image: "/chocolate-chip-cookies.jpg", // Add an image URL
      prepTime: "10 minutes",
      cookTime: "12 minutes",
      servings: 24
    }
  ];

  res.status(200).json(recipes);
}

Important: The `pages/api` directory is for server-side API routes. The code inside `recipes.js` will not be executed on the client-side. It’s used to provide the data that our frontend will consume.

Displaying Recipe Data on the Homepage

Now, let’s fetch and display this recipe data on our homepage (`pages/index.js`). Replace the content of `pages/index.js` with the following code:

// pages/index.js
import { useState, useEffect } from 'react';

function HomePage() {
  const [recipes, setRecipes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchRecipes() {
      try {
        const response = await fetch('/api/recipes');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setRecipes(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchRecipes();
  }, []);

  if (loading) return <p>Loading recipes...</p>;
  if (error) return <p>Error loading recipes: {error.message}</p>;

  return (
    <div>
      <h1>Recipe App</h1>
      <div style="{{">
        {recipes.map(recipe => (
          <div style="{{">
            <img src="{recipe.image}" alt="{recipe.name}" style="{{" />
            <h2>{recipe.name}</h2>
            <p><b>Prep Time:</b> {recipe.prepTime}</p>
            <p><b>Cook Time:</b> {recipe.cookTime}</p>
            <p><b>Servings:</b> {recipe.servings}</p>
            <button style="{{">View Recipe</button>
          </div>
        ))}
      </div>
    </div>
  );
}

export default HomePage;

Let’s break down this code:

  • Importing useState and useEffect: We import these React hooks to manage state and handle side effects (like fetching data).
  • useState: We use `useState` to create three state variables: `recipes` (an array to store the recipe data), `loading` (a boolean to indicate if data is being fetched), and `error` (to store any errors that occur during the fetch).
  • useEffect: The `useEffect` hook runs after the component renders. We use it to fetch the data from our API route (`/api/recipes`).
  • fetchRecipes function: This async function uses the `fetch` API to get the data from `/api/recipes`. It handles error checking and updates the `recipes`, `loading`, and `error` states accordingly.
  • Conditional Rendering: We use conditional rendering (`if (loading) …` and `if (error) …`) to display loading messages or error messages while the data is being fetched.
  • Mapping Recipes: We use the `map` function to iterate over the `recipes` array and render a separate `div` for each recipe. Each div displays the recipe name, image, prep time, cook time, servings, and a “View Recipe” button.

Running Your Application

To run your application, open your terminal and navigate to your project directory (`recipe-app`). Then, run the following command:

npm run dev

This will start the development server. Open your web browser and go to `http://localhost:3000`. You should see a list of recipes displayed on the homepage. If you see the recipes and no errors, congratulations! You have successfully displayed data fetched from a simple API.

Adding Recipe Details Page

Currently, clicking the “View Recipe” button does nothing. Let’s create a recipe details page to show the full recipe information. We’ll create a new page in the `pages` directory called `recipe/[id].js`. The square brackets `[id]` indicate a dynamic route, meaning that this page will handle requests like `/recipe/1`, `/recipe/2`, etc.

Create a file named `[id].js` inside the `pages/recipe` directory. Add the following code:

// pages/recipe/[id].js
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchRecipe() {
      try {
        const response = await fetch('/api/recipes');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const recipes = await response.json();
        const foundRecipe = recipes.find(recipe => recipe.id === parseInt(id));
        setRecipe(foundRecipe);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    if (id) {
      fetchRecipe();
    }
  }, [id]);

  if (loading) return <p>Loading recipe...</p>;
  if (error) return <p>Error loading recipe: {error.message}</p>;
  if (!recipe) return <p>Recipe not found</p>;

  return (
    <div>
      <h1>{recipe.name}</h1>
      <img src="{recipe.image}" alt="{recipe.name}" style="{{" />
      <h2>Ingredients:</h2>
      <ul>
        {recipe.ingredients.map((ingredient, index) => (
          <li>{ingredient}</li>
        ))}
      </ul>
      <h2>Instructions:</h2>
      <ol>
        {recipe.instructions.map((instruction, index) => (
          <li>{instruction}</li>
        ))}
      </ol>
    </div>
  );
}

export default RecipeDetails;

Let’s break down the code:

  • Importing useRouter: We import `useRouter` from `next/router` to access the route parameters (the `id` in our case).
  • useRouter: We use `useRouter()` to get the router object.
  • router.query: The `router.query` object contains the route parameters. In our case, `router.query.id` will contain the recipe ID (e.g., “1”, “2”, etc.).
  • Fetching the Recipe Data: We fetch all recipes from `/api/recipes` and find the recipe with the matching ID.
  • Conditional Rendering: We handle loading, error, and recipe-not-found states.
  • Displaying Recipe Details: We display the recipe name, image, ingredients, and instructions.

Now, let’s update the `pages/index.js` file to link to the recipe details page. Modify the “View Recipe” button to include a link to the corresponding recipe detail page:

// pages/index.js
import { useState, useEffect } from 'react';
import Link from 'next/link'; // Import the Link component

function HomePage() {
  const [recipes, setRecipes] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchRecipes() {
      try {
        const response = await fetch('/api/recipes');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const data = await response.json();
        setRecipes(data);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    fetchRecipes();
  }, []);

  if (loading) return <p>Loading recipes...</p>;
  if (error) return <p>Error loading recipes: {error.message}</p>;

  return (
    <div>
      <h1>Recipe App</h1>
      <div style="{{">
        {recipes.map(recipe => (
          <div style="{{">
            <img src="{recipe.image}" alt="{recipe.name}" style="{{" />
            <h2>{recipe.name}</h2>
            <p><b>Prep Time:</b> {recipe.prepTime}</p>
            <p><b>Cook Time:</b> {recipe.cookTime}</p>
            <p><b>Servings:</b> {recipe.servings}</p>
              {/* Use Link component */}
              <button style="{{">View Recipe</button>
            
          </div>
        ))}
      </div>
    </div>
  );
}

export default HomePage;

Here, we’ve added the `Link` component from `next/link`. We wrap the button with the `Link` component, and set the `href` attribute to `/recipe/${recipe.id}`. The `passHref` prop is important to pass the href to the child (the button).

Now, when you click the “View Recipe” button, you will be directed to the recipe details page for that specific recipe.

Styling Your Application

While the application is functional, it could use some styling to improve its appearance. Next.js offers several ways to style your application:

  • CSS Modules: These allow you to scope your CSS to individual components, preventing style conflicts.
  • Styled-JSX: A library that lets you write CSS directly in your JavaScript files, using a similar syntax to CSS-in-JS solutions.
  • Global CSS: You can add global styles to your application by importing a CSS file in `_app.js`.
  • CSS Frameworks: You can integrate popular CSS frameworks like Bootstrap, Tailwind CSS, or Material UI.

For simplicity, let’s add some basic styling using inline styles in our components and a bit of global CSS. First, create a file called `global.css` in the `styles` directory (create this directory if it doesn’t exist). Add the following CSS:

/* styles/global.css */
body {
  font-family: sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f4f4f4;
}

h1, h2 {
  text-align: center;
  color: #333;
}

Then, import this CSS file into `_app.js`:

// pages/_app.js
import '../styles/global.css';

function MyApp({ Component, pageProps }) {
  return 
}

export default MyApp;

You can also use inline styles within your components. For example, you can add some styles to the recipe details page:


// pages/recipe/[id].js
import { useRouter } from 'next/router';
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    async function fetchRecipe() {
      try {
        const response = await fetch('/api/recipes');
        if (!response.ok) {
          throw new Error(`HTTP error! status: ${response.status}`);
        }
        const recipes = await response.json();
        const foundRecipe = recipes.find(recipe => recipe.id === parseInt(id));
        setRecipe(foundRecipe);
      } catch (err) {
        setError(err);
      } finally {
        setLoading(false);
      }
    }

    if (id) {
      fetchRecipe();
    }
  }, [id]);

  if (loading) return <p>Loading recipe...</p>;
  if (error) return <p>Error loading recipe: {error.message}</p>;
  if (!recipe) return <p>Recipe not found</p>;

  return (
    <div style="{{">
      <h1 style="{{">{recipe.name}</h1>
      <img src="{recipe.image}" alt="{recipe.name}" style="{{" />
      <h2 style="{{">Ingredients:</h2>
      <ul style="{{">
        {recipe.ingredients.map((ingredient, index) => (
          <li style="{{">{ingredient}</li>
        ))}
      </ul>
      <h2 style="{{">Instructions:</h2>
      <ol style="{{">
        {recipe.instructions.map((instruction, index) => (
          <li style="{{">{instruction}</li>
        ))}
      </ol>
    </div>
  );
}

export default RecipeDetails;

Feel free to experiment with different styling approaches to customize the look and feel of your application.

Common Mistakes and How to Fix Them

Building a Next.js application can sometimes lead to common pitfalls. Here are some of them and how to avoid them:

  • Incorrect API Route Usage: Remember that code inside the `pages/api` directory runs on the server-side, not the client-side. Avoid directly importing API route files into your client-side components. Instead, use the `fetch` API to make requests to your API routes.
  • Missing or Incorrect Environment Variables: When working with API keys or other sensitive information, use environment variables. Create a `.env.local` file in your project root and store your keys there. Avoid hardcoding sensitive information directly in your code.
  • Incorrect Pathing: Double-check your file paths, especially when importing images or other assets. Relative paths can be tricky.
  • Not Handling Errors Properly: Always include error handling in your `fetch` calls. Check the `response.ok` property and handle potential errors gracefully. Provide informative error messages to the user.
  • Ignoring SEO Best Practices: Next.js is great for SEO, but you still need to optimize your content. Use descriptive titles and meta descriptions, include alt text for images, and structure your content with proper headings.

Key Takeaways

In this tutorial, we’ve covered the fundamentals of building a simple, interactive recipe application using Next.js. You’ve learned about:

  • Project Setup: How to create a new Next.js project.
  • Data Fetching: How to fetch data from an API route and display it on the page.
  • Dynamic Routes: How to create dynamic routes using the `[id].js` file structure.
  • State Management: Using `useState` and `useEffect` to manage data and handle side effects.
  • Styling: Basic styling techniques to improve the look of your application.

Summary / Key Takeaways

You’ve seen how to build a basic recipe app using Next.js. You can expand on this by adding features like user authentication, recipe search, filtering, favoriting, and more. This project provides a solid foundation for building more complex web applications with Next.js. Remember to practice, experiment, and continue learning to master the framework.

By using Next.js, you’ve created an application that is not only functional but also benefits from features like server-side rendering, improved SEO, and a better user experience. Next.js streamlines the development process, making it easier to build and maintain modern web applications. The skills you’ve gained in this project can be applied to a wide range of web development tasks, from building personal portfolios to creating full-fledged e-commerce platforms. Keep building, keep learning, and keep exploring the endless possibilities of web development!