Integrate Firebase with a Next.js app

1. Before you begin

In this codelab, you'll learn how to integrate Firebase with a Next.js web app called Friendly Eats, which is a website for restaurant reviews.

Friendly Eats web app

The completed web app offers useful features that demonstrate how Firebase can help you build Next.js apps. These features include the following:

  • Sign-in with Google and sign-out functionality: The completed web app lets you sign in with Google and sign out. User login and persistence is managed entirely through Firebase Authentication.
  • Images: The completed web app lets signed-in users upload restaurant images. Image assets are stored in Cloud Storage for Firebase. The Firebase JavaScript SDK provides a public URL to uploaded images. This public URL is then stored in the relevant restaurant document in Cloud Firestore.
  • Reviews: The completed web app lets signed-in users post reviews of restaurants that consist of a star rating and a text-based message. Review information is stored in Cloud Firestore.
  • Filters: The completed web app lets signed-in users filter the list of restaurants based on category, location, and price. You can also customize the sorting method used. Data is accessed from Cloud Firestore, and Firestore queries are applied based on the filters used.

Prerequisites

  • Knowledge of Next.js and JavaScript

What you'll learn

  • How to use Firebase with the Next.js App Router and server-side rendering.
  • How to persist images in Cloud Storage for Firebase.
  • How to read and write data in a Cloud Firestore database.
  • How to use sign-in with Google with the Firebase JavaScript SDK.

What you'll need

  • Git
  • The Java Development Kit
  • A recent stable version of Node.js
  • A browser of your choice, such as Google Chrome
  • A development environment with a code editor and terminal
  • A Google account for the creation and management of your Firebase project
  • The ability to upgrade your Firebase project to the Blaze pricing plan

2. Set up your development environment

This codelab provides the app's starter codebase and relies on the Firebase CLI.

Download the repository

  1. In your terminal, clone the codelab's GitHub repository:
    git clone https://github.com/firebase/friendlyeats-web.git
    
  2. The GitHub repository contains sample projects for multiple platforms. However, this codelab uses only the nextjs-start directory. Take note of the following directories:
    • nextjs-start: contains the starter code upon which you build.
    • nextjs-end: contains the solution code for the finished web app.
  3. In your terminal, navigate to the nextjs-start directory and install the necessary dependencies:
    cd friendlyeats-web/nextjs-start
    npm install
    

Install or update the Firebase CLI

Run the following command to verify that you have the Firebase CLI installed and that it's v12.5.4 or higher:

firebase --version
  • If you have the Firebase CLI installed, but it's not v12.5.4 or higher, update it:
    npm update -g firebase-tools
    
  • If you don't have the Firebase CLI installed, install it:
    npm install -g firebase-tools
    

If you're unable to install the Firebase CLI because of permission errors, see the npm documentation or use another installation option.

Log in to Firebase

  1. Run the following command to log in to the Firebase CLI:
    firebase login
    
  2. Depending on whether you want Firebase to collect data, enter Y or N.
  3. In your browser, select your Google account, and then click Allow.

3. Set up your Firebase project

In this section, you'll set up a Firebase project and associate a Firebase web app with it. You'll also set up the Firebase services used by the sample web app.

Create a Firebase project

  1. In the Firebase console, click Create project.
  2. In the Enter your project name text box, enter FriendlyEats Codelab (or a project name of your choice), and then click Continue.
  3. For this codelab, you don't need Google Analytics, so toggle off the Enable Google Analytics for this project option.
  4. Click Create project.
  5. Wait for your project to provision, and then click Continue.
  6. In your Firebase project, go to Project Settings. Note your project ID because you need it later. This unique identifier is how your project is identified (for example, in the Firebase CLI).

Add a web app to your Firebase project

  1. Navigate to your Project overview in your Firebase project, and then click e41f2efdd9539c31.png Web.
  2. In the App nickname text box, enter a memorable app nickname, such as My Next.js app.
  3. Select the Also set up Firebase Hosting for this app checkbox.
  4. Click Register app > Next > Next > Continue to console.

Upgrade your Firebase pricing plan

To use web frameworks, your Firebase project needs to be on the Blaze pricing plan, which means it's associated with a Cloud Billing account.

  • A Cloud Billing account requires a payment method, like a credit card.
  • If you're new to Firebase and Google Cloud, check if you're eligible for a $300 credit and a Free Trial Cloud Billing account.

However, note that completion of this codelab shouldn't incur any actual charges.

To upgrade your project to the Blaze plan, follow these steps:

  1. In the Firebase console, select to upgrade your plan.
  2. In the dialog, select the Blaze plan, and then follow the on-screen instructions to associate your project with a Cloud Billing account.
    If you needed to create a Cloud Billing account, you might need to navigate back to the upgrade flow in the Firebase console to complete the upgrade.

Set up Firebase services in the Firebase console

Set up Authentication

  1. In the Firebase console, navigate to Authentication.
  2. Click Get started.
  3. In the Additional providers column, click Google > Enable.
  4. In the Public-facing name for project text box, enter a memorable name, such as My Next.js app.
  5. From the Support email for project drop-down, select your email address.
  6. Click Save.

Set up Cloud Firestore

  1. In the Firebase console, navigate to Firestore.
  2. Click Create database > Start in test mode > Next.
    Later in this codelab, you'll add Security Rules to secure your data. Do not distribute or expose an app publicly without adding Security Rules for your database.
  3. Use the default location or select a location of your choice.
    For a real app, you want to choose a location that's close to your users. Note that this location cannot be changed later, and it will also automatically be the location of your default Cloud Storage bucket (next step).
  4. Click Done.

Set up Cloud Storage for Firebase

  1. In the Firebase console, navigate to Storage.
  2. Click Get started > Start in test mode > Next.
    Later in this codelab, you'll add Security Rules to secure your data. Do not distribute or expose an app publicly without adding Security Rules for your Storage bucket.
  3. The location of your bucket should already be selected (due to setting up Firestore in the previous step).
  4. Click Done.

4. Review the starter codebase

In this section, you'll review a few areas of the app's starter codebase to which you'll add functionality in this codelab.

Folder and file structure

The following table contains an overview of the folder and file structure of the app:

Folders and files

Description

src/components

React components for filters, headers, restaurant details, and reviews

src/lib

Utility functions that aren't necessarily bound to React or Next.js

src/lib/firebase

Firebase-specific code and Firebase configuration

public

Static assets in the web app, like icons

src/app

Routing with the Next.js App Router

src/app/restaurant

An API route handler

package.json and package-lock.json

Project dependencies with npm

next.config.js

Next.js-specific configuration (server actions are enabled)

jsconfig.json

JavaScript language-service configuration

Server and client components

The app is a Next.js web app that uses the App Router. Server rendering is used throughout the app. For example, the src/app/page.js file is a server component responsible for the main page. The src/components/RestaurantListings.jsx file is a client component denoted by the "use client" directive at the beginning of the file.

Import statements

You might notice import statements like the following:

import RatingPicker from "@/src/components/RatingPicker.jsx";

The app uses the @ symbol to avoid clunky relative import paths and is made possible by path aliases.

Firebase-specific APIs

All Firebase API code is wrapped in the src/lib/firebase directory. Individual React components then import the wrapped functions from the src/lib/firebase directory, rather than importing Firebase functions directly.

Mock data

Mock restaurant and review data is contained in the src/lib/randomData.js file. Data from that file is assembled in the code in the src/lib/fakeRestaurants.js file.

5. Set up local hosting with the Firebase Hosting emulator

In this section, you'll use the Firebase Hosting emulator to run the Next.js web app locally.

By the end of this section, the Firebase Hosting emulator runs the Next.js app for you, so you don't need to run Next.js in a separate process to the emulators.

Download and use a Firebase service account

The web app you'll build in this codelab uses server side rendering with Next.js.

The Firebase Admin SDK for Node.js is used to ensure Security Rules are functional from server side code. To use APIs in Firebase Admin, you need to download and use a Firebase service account from the Firebase console.

  1. In the Firebase console, navigate to the Service Accounts page in your Project settings.
  2. Click Generate new private key > Generate Key.
  3. After the file has downloaded to your filesystem, get the full path to that file.
    For example, if you downloaded the file to your Downloads directory, the full path might look like this: /Users/me/Downloads/my-project-id-firebase-adminsdk-123.json
  4. In your terminal, set the GOOGLE_APPLICATION_CREDENTIALS environment variable to the path of your downloaded private key. In a Unix environment, the command might look like this:
    export GOOGLE_APPLICATION_CREDENTIALS="/Users/me/Downloads/my-project-id-firebase-adminsdk-123.json"
    
  5. Keep this terminal open and use it for the remainder of this codelab, as your environment variable might be lost if you start a new terminal session.
    If you open a new terminal session, you must rerun the previous command.

Add your Firebase configuration to your web app code

  1. In the Firebase console, navigate to your Project settings.
  2. In the SDK setup and configuration pane, find the firebaseConfig variable, and copy its properties and their values.
  3. Open the .env file in your code editor, and fill in the environment variable values with the configuration values from the Firebase console.
  4. In the file, replace the existing properties with those that you copied.
  5. Save the file.

Initialize the web app with your Firebase project

To connect the web app to your Firebase project, follow these steps:

  1. In your terminal, ensure that web frameworks are enabled in Firebase:
    firebase experiments:enable webframeworks
    
  2. Initialize Firebase:
    firebase init
    
  3. Select the following options:
    • Firestore: Configure security rules and indexes files for Firestore
    • Hosting: Configure files for Firebase Hosting and (optionally) set up GitHub Action deploys
    • Storage: Configure a security rules file for Cloud Storage
    • Emulators: Set up local emulators for Firebase products
  4. Select Use an existing project, and then enter the project ID that you previously noted.
  5. Select the default values for all subsequent questions until you reach the question In which region would you like to host server-side content, if applicable?. The terminal displays a message that it detects an existing Next.js codebase in the current directory.
  6. For the question In which region would you like to host server-side content, if applicable?, select the location that you previously selected for Firestore and Cloud Storage.
  7. Select the default values for all subsequent questions until you reach the question Which Firebase emulators do you want to set up?. For this question, select Functions emulator and Hosting emulator.
  8. Select the default values for all other questions.

Deploy Security Rules

The code already has sets of Security Rules for Firestore and for Cloud Storage for Firebase. After you deploy the Security Rules, the data in your database and your bucket are better protected from misuse.

  1. To deploy these Security Rules, run this command in your terminal:
    firebase deploy --only firestore:rules,storage
    
  2. If you're asked: "Cloud Storage for Firebase needs an IAM Role to use cross-service rules. Grant the new role?", select Yes.

Start the Hosting emulator

  1. In your terminal, start the Hosting emulator:
    firebase emulators:start --only hosting
    
    Your terminal responds with the port where you can find the Hosting emulator, for example http://localhost:5000/.

Terminal showing that the Hosting Emulator is ready

  1. In your browser, navigate to the URL with the Firebase Hosting emulator.
  2. If you see the error in the web page that starts like this: "Error: Firebase session cookie has incorrect...", you need to delete all your cookies in your localhost environment. To do this, follow the instructions in delete cookies | DevTools Documentation.

A cookie session error

Deleting cookies in DevTools

Now you can see the initial web app! Even though you're viewing the web app on a localhost URL, it uses real Firebase services that you configured in your console.

6. Add authentication to the web app

In this section, you add authentication to the web app so that you can log in to it.

Implement the sign-in and sign-out functions

  1. In the src/lib/firebase/auth.js file, replace the onAuthStateChanged, signInWithGoogle, and signOut functions with the following code:
export function onAuthStateChanged(cb) {
        return _onAuthStateChanged(auth, cb);
}

export async function signInWithGoogle() {
        const provider = new GoogleAuthProvider();

        try {
                await signInWithPopup(auth, provider);
        } catch (error) {
                console.error("Error signing in with Google", error);
        }
}

export async function signOut() {
        try {
                return auth.signOut();
        } catch (error) {
                console.error("Error signing out with Google", error);
        }
}

This code uses the following Firebase APIs:

Firebase API

Description

GoogleAuthProvider

Creates a Google authentication provider instance.

signInWithPopup

Starts a dialog-based authentication flow.

auth.signOut

Signs out the user.

In the src/components/Header.jsx file, the code already invokes the signInWithGoogle and signOut functions.

  1. In the web app, refresh the page and click Sign in with Google. The web app doesn't update, so it's unclear whether sign-in succeeded.

Subscribe to authentication changes

To subscribe to authentication changes, follow these steps:

  1. Navigate to the src/components/Header.jsx file.
  2. Replace the useUserSession function with the following code:
function useUserSession(initialUser) {
        // The initialUser comes from the server through a server component
        const [user, setUser] = useState(initialUser);
        const router = useRouter();

        useEffect(() => {
                const unsubscribe = onAuthStateChanged(authUser => {
                        setUser(authUser);
                });
                return () => {
                        unsubscribe();
                };
        }, []);

        useEffect(() => {
                onAuthStateChanged(authUser => {
                        if (user === undefined) return;
                        if (user?.email !== authUser?.email) {
                                router.refresh();
                        }
                });
        }, [user]);

        return user;
}

This code uses a React state hook to update the user when the onAuthStateChanged function specifies that there's a change to the authentication state.

Verify changes

The root layout in the src/app/layout.js file renders the header and passes in the user, if available, as a prop.

<Header initialUser={currentUser?.toJSON()} />

This means that the <Header> component renders user data, if available, during server run time. If there are any authentication updates during the page lifecycle after initial page load, the onAuthStateChanged handler handles them.

Now it's time to test the web app and verify what you built.

To verify the new authentication behavior, follow these steps:

  1. In your browser, refresh the web app. Your display name appears in the header.
  2. Sign out and sign in again. The page updates in real-time without a page refresh. You can repeat this step with different users.
  3. Optional: Right-click the web app, select View page source, and search for the display name. It appears in the raw HTML source returned from the server.

7. View restaurant information

The web app includes mock data for restaurants and reviews.

Add one or more restaurants

To insert mock restaurant data into your local Cloud Firestore database, follow these steps:

  1. In the web app, select 2cf67d488d8e6332.png > Add sample restaurants.
  2. In the Firebase console on the Firestore Database page, select restaurants. You see the top-level documents in the restaurant collection, each of which represents a restaurant.
  3. Click a few documents to explore the properties of a restaurant document.

Display the list of restaurants

Your Cloud Firestore database now has restaurants that the Next.js web app can display.

To define the data-fetching code, follow these steps:

  1. In the src/app/page.js file, find the <Home /> server component, and review the call to the getRestaurants function, which retrieves a list of restaurants at server run time. You implement the getRestaurants function in the following steps.
  2. In the src/lib/firebase/firestore.js file, replace the applyQueryFilters and getRestaurants functions with the following code:
function applyQueryFilters(q, { category, city, price, sort }) {
        if (category) {
                q = query(q, where("category", "==", category));
        }
        if (city) {
                q = query(q, where("city", "==", city));
        }
        if (price) {
                q = query(q, where("price", "==", price.length));
        }
        if (sort === "Rating" || !sort) {
                q = query(q, orderBy("avgRating", "desc"));
        } else if (sort === "Review") {
                q = query(q, orderBy("numRatings", "desc"));
        }
        return q;
}

export async function getRestaurants(filters = {}) {
        let q = query(collection(db, "restaurants"));

        q = applyQueryFilters(q, filters);
        const results = await getDocs(q);
        return results.docs.map(doc => {
                return {
                        id: doc.id,
                        ...doc.data(),
                        // Only plain objects can be passed to Client Components from Server Components
                        timestamp: doc.data().timestamp.toDate(),
                };
        });
}
  1. Refresh the web app. Restaurant images appear as tiles on the page.

Verify that the restaurant listings load at server run time

Using the Next.js framework, it might not be obvious when data is loaded at server run time or client-side run time.

To verify that restaurant listings load at server run time, follow these steps:

  1. In the web app, open DevTools and disable JavaScript.

Disable JavaScipt in DevTools

  1. Refresh the web app. The restaurant listings still load. Restaurant information is returned in the server response. When JavaScript is enabled, the restaurant information is hydrated through the client-side JavaScript code.
  2. In DevTools, re-enable JavaScript.

Listen for restaurant updates with Cloud Firestore snapshot listeners

In the previous section, you saw how the initial set of restaurants loaded from the src/app/page.js file. The src/app/page.js file is a server component and is rendered on the server, including the Firebase data-fetching code.

The src/components/RestaurantListings.jsx file is a client component and can be configured to hydrate server-rendered markup.

To configure the src/components/RestaurantListings.jsx file to hydrate server-rendered markup, follow these steps:

  1. In the src/components/RestaurantListings.jsx file, observe the following code, which is already written for you:
useEffect(() => {
        const unsubscribe = getRestaurantsSnapshot(data => {
                setRestaurants(data);
        }, filters);

        return () => {
                unsubscribe();
        };
}, [filters]);

This code invokes the getRestaurantsSnapshot() function, which is similar to the getRestaurants() function that you implemented in a previous step. However this snapshot function provides a callback mechanism so that the callback is invoked every time a change is made to the restaurant's collection.

  1. In the src/lib/firebase/firestore.js file, replace the getRestaurantsSnapshot() function with the following code:
export function getRestaurantsSnapshot(cb, filters = {}) {
        if (typeof cb !== "function") {
                console.log("Error: The callback parameter is not a function");
                return;
        }

        let q = query(collection(db, "restaurants"));
        q = applyQueryFilters(q, filters);

        const unsubscribe = onSnapshot(q, querySnapshot => {
                const results = querySnapshot.docs.map(doc => {
                        return {
                                id: doc.id,
                                ...doc.data(),
                                // Only plain objects can be passed to Client Components from Server Components
                                timestamp: doc.data().timestamp.toDate(),
                        };
                });

                cb(results);
        });

        return unsubscribe;
}

Changes made through the Firestore Database page now reflect in the web app in real time.

  1. In the web app, select 27ca5d1e8ed8adfe.png > Add sample restaurants. If your snapshot function is implemented correctly, the restaurants appear in real-time without a page refresh.

8. Save user data from the web app

  1. In the src/lib/firebase/firestore.js file, replace the updateWithRating() function with the following code:
const updateWithRating = async (
        transaction,
        docRef,
        newRatingDocument,
        review
) => {
        const restaurant = await transaction.get(docRef);
        const data = restaurant.data();
        const newNumRatings = data?.numRatings ? data.numRatings + 1 : 1;
        const newSumRating = (data?.sumRating || 0) + Number(review.rating);
        const newAverage = newSumRating / newNumRatings;

        transaction.update(docRef, {
                numRatings: newNumRatings,
                sumRating: newSumRating,
                avgRating: newAverage,
        });

        transaction.set(newRatingDocument, {
                ...review,
                timestamp: Timestamp.fromDate(new Date()),
        });
};

This code inserts a new Firestore document representing the new review. The code also updates the existing Firestore document that represents the restaurant with updated figures for the number of ratings and the average calculated rating.

  1. Replace the addReviewToRestaurant() function with the following code:
export async function addReviewToRestaurant(db, restaurantId, review) {
        if (!restaurantId) {
                throw new Error("No restaurant ID was provided.");
        }

        if (!review) {
                throw new Error("A valid review has not been provided.");
        }

        try {
                const docRef = doc(collection(db, "restaurants"), restaurantId);
                const newRatingDocument = doc(
                        collection(db, `restaurants/${restaurantId}/ratings`)
                );

                await runTransaction(db, transaction =>
                        updateWithRating(transaction, docRef, newRatingDocument, review)
                );
        } catch (error) {
                console.error(
                        "There was an error adding the rating to the restaurant.",
                        error
                );
                throw error;
        }
}

Implement a Next.js Server Action

A Next.js Server Action provides a convenient API to access form data, such as data.get("text") to get the text value from the form submission payload.

To use a Next.js Server Action to process the review form submission, follow these steps:

  1. In the src/components/ReviewDialog.jsx file, find the action attribute in the <form> element.
<form action={handleReviewFormSubmission}>

The action attribute value refers to a function that you implement in the next step.

  1. In the src/app/actions.js file, replace the handleReviewFormSubmission() function with the following code:
// This is a next.js server action, which is an alpha feature, so
// use with caution.
// https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions
export async function handleReviewFormSubmission(data) {
        const { app } = await getAuthenticatedAppForUser();
        const db = getFirestore(app);

        await addReviewToRestaurant(db, data.get("restaurantId"), {
                text: data.get("text"),
                rating: data.get("rating"),

                // This came from a hidden form field.
                userId: data.get("userId"),
        });
}

Add reviews for a restaurant

You implemented support for review submissions, so now you can verify that your reviews are inserted into Cloud Firestore correctly.

To add a review and verify that it's inserted into Cloud Firestore, follow these steps:

  1. In the web app, select a restaurant from the home page.
  2. On the restaurant's page, click 3e19beef78bb0d0e.png.
  3. Select a star rating.
  4. Write a review.
  5. Click Submit. Your review appears at the top of the list of reviews.
  6. In Cloud Firestore, search the Add document pane for the document of the restaurant that you reviewed and select it.
  7. In the Start collection pane, select ratings.
  8. In the Add document pane, find the document for your review to verify that it was inserted as expected.

Documents in the Firestore Emulator

9. Save user-uploaded files from the web app

In this section, you add functionality so that you can replace the image associated with a restaurant when you're logged in. You upload the image to Firebase Storage, and update the image URL in the Cloud Firestore document that represents the restaurant.

To save user-uploaded files from the web app, follow these steps:

  1. In the src/components/Restaurant.jsx file, observe the code that runs when the user uploads a file:
async function handleRestaurantImage(target) {
        const image = target.files ? target.files[0] : null;
        if (!image) {
                return;
        }

        const imageURL = await updateRestaurantImage(id, image);
        setRestaurant({ ...restaurant, photo: imageURL });
}

No changes are needed, but you implement the behavior of the updateRestaurantImage() function in the following steps.

  1. In the src/lib/firebase/storage.js file, replace the updateRestaurantImage() and uploadImage() functions with the following code:
export async function updateRestaurantImage(restaurantId, image) {
        try {
                if (!restaurantId)
                        throw new Error("No restaurant ID has been provided.");

                if (!image || !image.name)
                        throw new Error("A valid image has not been provided.");

                const publicImageUrl = await uploadImage(restaurantId, image);
                await updateRestaurantImageReference(restaurantId, publicImageUrl);

                return publicImageUrl;
        } catch (error) {
                console.error("Error processing request:", error);
        }
}

async function uploadImage(restaurantId, image) {
        const filePath = `images/${restaurantId}/${image.name}`;
        const newImageRef = ref(storage, filePath);
        await uploadBytesResumable(newImageRef, image);

        return await getDownloadURL(newImageRef);
}

The updateRestaurantImageReference() function is already implemented for you. This function updates an existing restaurant document in Cloud Firestore with an updated image URL.

Verify the image-upload functionality

To verify that the image uploads as expected, follow these steps:

  1. In the web app, verify that you're logged in and select a restaurant.
  2. Click 7067eb41fea41ff0.png and upload an image from your filesystem. Your image leaves your local environment and is uploaded to Cloud Storage. The image appears immediately after you upload it.
  3. Navigate to Cloud Storage for Firebase.
  4. Navigate to the folder that represents the restaurant. The image that you uploaded exists in the folder.

6cf3f9e2303c931c.png

10. Conclusion

Congratulations! You learned how to use Firebase to add features and functionality to a Next.js app. Specifically, you used the following:

Learn more