How To Structure / Model Firebase Cloud Firestore ← Vue.js

Last modified on April 6th, 2021
Raja Tamil
Firebase Vue.js

Building a collection of list items organized by sections is one of the scenarios that you can’t avoid when building any type of application. In this guide, I will be showing you exactly how to do that by building a simple restaurant menu page using Vue.js and Firebase Cloud Firestore.

Along the way, you’re also going to learn how to properly structure and query Cloud Firestore Database for it.

Let’s take a look at what we will be building by the end of this tutorial.

vuejs-firebase-build-a-restaurant-menu-page

In the typical example above, I have a restaurant name at the top. Below that you can see menu items grouped by sections/types such as Appetizers, Dum Briyani, etc.

Pretty straight forward.

Final JavaScript Object API Structure 

Before going further, I would like to talk about the structure of the final JavaScript object that will be needed to build this restaurant menu page.

The picture below depicts a high-level overview of how to transform the final JavaScript object into the actual restaurant menu page by showing which data goes where.

To generate this object, simply get the data from Cloud Firestore and merge them together which is very similar to API output when fetching data from HTTP request).

Let’s take a look at the output JavaScript object in detail.

It has title and id properties at the top level as well as a menu property which is an array that contains a couple of objects which will be the restaurant sections/types.

Each section/type object in the menu array has a title property and menuItems which is another array of objects. Each object inside the menu items consists of title, description and price properties.

Data Structure For The Menu Page

I could create a data structure very similar to the output JavaScript object, but it will have deeply nested data inside a single document which must be avoided at any cost.

Instead, I will be making a sub-collection wherever new data will be added over the period of time. Then, query & merge them together in order to match the final JavaScript object.

Cloud Firestore queries are shallow, so when you make a query to a document, it will not have a sub-collection on it.

Recommended Learn Firestore CRUD Queries Quickly [Guide]

Here is the infographic that shows how to structure data for a restaurant menu page.

As you can see, I have a restaurant collection, which has a few documents. Each document has two fields: the title which holds string value and menu-types which is a sub-collection.

Documents on the menu-types sub-collection again have two fields; the title that has string value and menu-items which is another sub-collection.

Each document in that sub-collection has three fields which are title, description, and price.

Here is the screenshot of an actual Cloud Firestore database structure with sample data.

Add some data similar to the above structure to your Cloud Firestore Database so that it’s easy to follow along.

Create A Vue.js Project

1. Go ahead and create a vue.js project using webpack.

vue init webpack my-project

2. Then, npm install to it.

npm install

3. Run the application.

npm run dev

4. Once the vue.js app is running, go to HelloWord.vue and delete the file.

5. App.vue is the file I will be putting all my code into to make it simpler for this tutorial.

6. Add Materialize CSS link to index.html which is optional.

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">

Initialize Firebase

1. Install Firebase.

npm install --save firebase

2. Initialize Firebase inside main.js.

import firebase from "firebase";

var firebaseConfig = {
  apiKey: "*************************",
  authDomain: "*************************",
  databaseURL: "*************************",
  projectId: "*************************",
  storageBucket: "*************************",
  messagingSenderId: "*************************",
  appId: "*************************"
};
firebase.initializeApp(firebaseConfig);

Get initialization code from your Firebase project on the Firebase console.

Query Restaurant Data

  1. Create a couple of properties restaurant and error inside data() method.
data() {
   return {
      restaurant: {},
      error:""
   };
},

2. DefinegetRestaurantData() function inside methods:{} object.

Add async keyword in front of the getRestaurantData() function as I will be making a few asynchronous operations inside it. As you can see, this function also takes a single parameter called resId.

Inside the try-catch block, declare function scoped object called restaurant which will have the final output JavaScript object.

methods: {
    async getRestaurantData(resId) {
        try {
            var restaurant = {};

            // Get Restaurant Data
            var resRef = firebase.firestore().collection("restaurants");
            var resSnap = await resRef.doc(resId).get();
            restaurant = resSnap.data();
            restaurant.id = resSnap.id;
            console.log(restaurant)
           
            return restaurant
        } catch (e) {
            return {
                errorMsg: "Something went wrong. Please Try Again."
            }
        }
    }
}

Get a reference to restaurant collection and store it in resRef. Then call doc()method on it by passing resId as an argument.

Run a get() method on it as well, which is an asynchronous call that returns a promise which will become fulfilled when a document snapshot is available on to the variable resSnap.

To pause the async operation, use await keyword in front of it which will make sure to wait until the restaurant snapshot (resSnap) is ready before executing further code and convert it to JavaScript object by running data() method on the resSnap.

Finally, assign the data to the restaurant object including document id and return it.

3. Call getRestaurantData() method inside created() {} function.

Inside created() method, invoke getRestaurantData() method by passing an actual restaurant ID as an argument which will return a promise as well.

To get a hold of the value, call the then method on that promise and pass an anonymous function to it which will have the returned data as a response.

created() {
    this.getRestaurantData("anjappar-chettinad").then(response => {
        if (response && response.errorMsg) {
            this.error = response.errorMsg;
        } else {
            this.restaurant = response;
        }
    });
},

Inside the callback function, check to see if the response has a specific property called errorMsg. If yes, assign errorMsg to the this.error property otherwise assign it to this.restaurant.

4. You may get an error message saying “Missing / Insufficient Permission” when querying Cloud Firestore on the browser console.

To fix that, go to Firebase Database Rules Tab and paste the following:

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read;
    }
  }
}

In a real-world scenario, you are going to have a security rule for each collection either root level or sub-collections.

Recommended 6 Must Know Firestore Security Rules

5. When you run the app at this point, you are going to see restaurant object with title and id properties on the console.

But why? What happened to the menu-types sub-collection? This happened because as I mentioned earlier Cloud Firestore queries are shallow, so it only retrieves documents with fields but no sub-collections.

To get sub-collections, you will have to make a separate query to get the menu-types (sections).

Recommended -> Learn Firestore CRUD Queries Quickly [Guide]

Retrieve & Merge Sub-Collections Data

1. Inside getRestaurantData() function, obtain a reference to menu-types sub-collection.

2. Run a get() method on it which will return a promise. This promise will become fulfilled when a menu-types snapshot is available.

Again, use await in front of the query in order to make it synchronous, which will stop executing the next statement until the snapshot becomes available.

// Get Restaurant Menu Types
var menuTypesSnap = await resRef
    .doc(resId)
    .collection("menu-types")
    .get();
restaurant.menu = [];

for (const menuTypeObj of menuTypesSnap.docs) {
    var menuType = menuTypeObj.data();
    menuType["menuItems"] = [];
    // Get Restaurant Menu Items
    var menuItemsSanp = await menuTypeObj.ref
        .collection("menu-items")
        .get();
    for (const menuItem of menuItemsSanp.docs) {
        menuType["menuItems"].push(menuItem.data());
    }
    restaurant.menu.push(menuType);
}

Then, create a property called menu inside the restaurant object and set it’s value to an empty array for now. This is where I am going to be adding menu sections/type objects.

Now, iterate through the menuTypeSnap using for-of and convert documents to JavaScript objects using data() on them and store them into the menuType object.

You could use forEach instead of for-of. But for-of will wait to finish the synchronous operation before going to the next interation which is exactly what I wanted.

Now, Let’s get the items from the menu-items collection and store them inside the menuItems property which is another array inside menuType object.

Let’s get a reference to the menu-items collection. Using the await keyword, it will pause the loop until the snapshot becomes available.

Once getting the menuItemsSnap, loop through them, convert each snapshot to JavaScript object and push it to the menuItems array which is the property of menuType object.

Finally, push menuType object to restaurant.menu property on each iteration.

At this stage, the scoped restaurant object will have the correct data structure that is needed to build the view.

Here is the full source code of getRestaurantData() code.

methods: {
    async getRestaurantData(resId) {
        try {
            var restaurant = {};

            // Get Restaurant Data
            var resRef = firebase.firestore().collection("restaurants");
            var resSnap = await resRef.doc(resId).get();
            restaurant = resSnap.data();
            restaurant.id = resSnap.id;


            // Get Restaurant Menu Types
            var menuTypesSnap = await resRef
                .doc(resId)
                .collection("menu-types")
                .get();
            restaurant.menu = [];

            for (const menuTypeObj of menuTypesSnap.docs) {
                var menuType = menuTypeObj.data();
                menuType["menuItems"] = [];
                // Get Restaurant Menu Items
                var menuItemsSanp = await menuTypeObj.ref
                    .collection("menu-items")
                    .get();
                for (const menuItem of menuItemsSanp.docs) {
                    menuType["menuItems"].push(menuItem.data());
                }
                restaurant.menu.push(menuType);
            }
            return restaurant;
        } catch (e) {
            return {
                errorMsg: "Something went wrong. Please Try Again."
            };
        }
    }
}

Recommended Build A Secure To-Do App with Vue + Firestore [Authentication]

Render Restaurant API Object To The View

Inside the container class, which is part of Materialize CSS, add a div element that is responsible to show an error if any.

Create li inside ul and show the restaurant title which is one of the top-level properties on the restaurant object.

Make a ul element for each section by looping through the restaurant.menu which is an array of objects (menu sections/types) and show the title of menu section/type using the menu.title property which will print Appetizers, Dum Briyani, etc.

<template>
    <div id="app">
        <div class="container">
            <div class="red" v-if="error">{{error}}</div>

            <ul class="collection with-header">
                <li class="collection-header #212121 grey darken-4 white-text">
                    <h4>{{restaurant.title}}</h4>
                </li>
            </ul>

            <ul class="collection with-header" v-for="menu in restaurant.menu" :key="menu.id">
                <li class="collection-item pink white-text">
                    <div>{{menu.title}}</div>
                </li>
                <li class="collection-item" v-for="item in menu.menuItems" :key="item.id">
                    <div>
                        {{item.title}}
                        <br />
                        <span class="grey-text">{{item.description}}</span>
                        <a href="#!" class="secondary-content">${{item.price}}</a>
                    </div>
                </li>
            </ul>
        </div>
    </div>
</template>

You will need to make a nested loop in order to show menu items on each section properly.

To do that, iterate through menu.menuItems array and pull title, description and price properties and show them to the appropriate place like in the code above.

If everything goes well, you should have a restaurant menu page similar to the screenshot below.

vuejs-firebase-build-a-restaurant-menu-page

Conclusion

In this quick tutorial, I showed you how to create a simple restaurant menu page that has menu items organized by section. Along the way, you learned how the data structure stored on the Cloud Firestore differs from the final output JavaScript object structure.

Finally, you understood how to query and merge data from Cloud Firestore using for-of loop and await-async to match the final JavaScript object which is then converted into the restaurant menu page.

Question for you
How would you structure your data when building an application similar to this?

If you have a different approach, feel free to share it in the comment section below…

I am looking forward to hearing from you…Thank you for reading…

Recommended Build A Secure To-Do App with Vue + Firestore [Authentication]