This page looks best with JavaScript enabled

Using Vue with Firestore

 ·   ·  ☕ 12 min read

Firestore can make up for a great backend application. You can get started quite easily, scale it up nicely, and in general, worry less about the “server” side of things. While it is quite easy to use Firestore as-is, using it in Vue opens up a whole new universe.

But, first - let’s go through the grind of knowing why we would want to do this? And, of course, to please everyone who just happens to be here reading the next exciting story on a lazy afternoon.

Firestore?

Firestore is a no SQL database by Google that is available on cloud. You can do exciting things like storing data and accessing data - all without maintaining a database of your own. If you are indeed truly delighted by this phenomenon -

  1. You have arrived to the great 2010’s when everything’s a web service - welcome! And oh, we crossed over to the new decade this year
  2. What exactly are you doing here? I mean here on this Blog, on earth, and at this moment? It is time to contemplate life I guess

That will be the last of the poor jokes, I promise. Onwards then.

Firestore is a God-send for -

  1. Small experiments and projects - you can get started for free, and have exactly zero ongoing server costs if you stay within limits
  2. Large projects that require rapid scalability
  3. Applications where you have enough problems and don’t want to add a server ecosystem to the mix (e.g. mobile apps)

Get started by visiting this link, signing up and head over to the console.

Let’s see some more of Firestore and Vue by creating a “totally not twitter” application. Users should be able to -

  • Tweet
  • Retweet
  • That’s pretty much it - this is not “twitter” after all

This is how it looks -

fwtr-app-demo

If you are not disappointed yet (did you really think we will build Twitter in one blog post?), read on - things are going to be set on 🔥.

Getting Started

Create a Vue project.

Install the Vue CLI if you have not done it before.

1
npm i -g @vue/cli

Create the project.

1
vue create fwtr

Answer a bunch of questions - we choose no to everything except Vue Router & Vuex store. vue-cli installs and configures everything for you.

Next, install vue-fire, an awesome package that provides a Vue wrapper for firebase. We could use Firebase/Firestore as-is, but Vuefire just makes things a bit easy.

1
npm i --save vuefire firebase

While at it, let’s install a small toast library to show alerts and messages.

1
npm i --save vue-toast-notification

Start your Vue project.

1
npm run serve

Head over to the Firestore console. Create a new project by clicking the big, bold button, and follow the guided wizard.

  1. Name the project
  2. Enable Google Analytics and select an Analytics account (optional)

You will see the below project page once you are done with the setup.

firestore-new-project

Click on Web icon to register your web app. We don’t need Firestore hosting, but feel free to go wild. Provide any name and click Register to see the API details. Copy over the details.

Go to Cloud Firestore link in your Firebase console and click on “Create Database”. Choose “Start in test mode”, select a region near you and hit “Enable” to create a new database.

In the database page, click on “Start collection” > enter “tweets” as the collection name, and save. You can optionally enter one or more records in the collection.

firestore-create-collection

You and your project are all fired up now.

Firestore configuration

In your project main.js, add two lines -

1
2
3
import { firestorePlugin } from "vuefire";

Vue.use(firestorePlugin);

We will also add the configuration required for the toast library.

1
2
3
4
5
6
import VueToast from "vue-toast-notification";
import "vue-toast-notification/dist/theme-default.css";
//  ...other code
Vue.use(VueToast, {
  position: "top-right",
});

Now, create a new file called db.js in your project root folder.

1
2
3
4
5
6
7
import firebase from "firebase/app";
import "firebase/firestore";

// Get a Firestore instance
export const db = firebase
  .initializeApp({ projectId: "fwtr-fwtr" })
  .firestore();

Replace fwtr-fwtr with your own project name - you would have provided the project name in the Firebase console.

Home Page

Let’s get back to our client app for a minute. First, let’s include a stylesheet - because, we (most of us) don’t live in caves no more.

Edit /public/index.html. Include the following statement to include an awesome library that automates your styling.

1
2
3
4
<!-- Code -->
<link rel="stylesheet" href="https://unpkg.com/chota@latest" />

<!-- Code -->

Chota.css is a minimal CSS framework that does not require you to “class” each and every element. It also provides a minimal grid and commonly used styled components.

Next, change the home page a bit to keep things interesting. Edit src/views/Home.vue.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
  <div id="app">
    <nav id="nav">
      <div class="nav-left">
        <a class="brand">Fwtr</a>
      </div>
      <div class="nav-right">
        <router-link to="/" class="nav-link">Home</router-link>
        <router-link to="/tweets" v-if="user.uid">Tweets</router-link>
        <router-link to="/signup" v-else>Signup</router-link>
        <router-link to="/login">User</router-link>
        <router-link to="/about">?</router-link>
      </div>
    </nav>
    <router-view />
  </div>
</template>

<script>
  import { mapState } from "vuex";

  export default {
    computed: {
      ...mapState(["user"]),
    },
  };
</script>

We will also include some style classes but those have been ignored here for the sake of brevity - see App.vue in the repo.

Add code for router /src/router/index.js -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home,
  },
  {
    path: "/about",
    name: "About",
    // route level code-splitting
    // this generates a separate chunk (about.[hash].js) for this route
    // which is lazy-loaded when the route is visited.
    component: function () {
      return import(/* webpackChunkName: "about" */ "../views/About.vue");
    },
  },
  {
    path: "/tweets",
    name: "Tweets",
    component: function () {
      return import("../views/Tweets.vue");
    },
  },
  {
    path: "/signup",
    name: "Signup",
    component: function () {
      return import("../views/Signup.vue");
    },
  },
  {
    path: "/login",
    name: "Login",
    component: function () {
      return import("../views/Login.vue");
    },
  },
];

const router = new VueRouter({
  mode: "history",
  routes,
});

export default router;

..and add some code to the store -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    user: {},
  },
  mutations: {
    setUser(state, val) {
      state.user = val;
      console.log("updated state", state.user);
    },
  },
  actions: {},
  modules: {},
});

You should have something like this by now..

fwtr-home

Authentication

A good fwtr application cannot exist without users who can shout everyone down and argue on significant pointless stuff. Enable users and authentication with the click or two in Firebase and Vue.

In your Firebase home page, select Authentication on the left tab bar. Navigate to Sign-in Method and enable authentication method. I have selected Email/Password as the only sign-in provider.

firestore-auth

Back to your client app, let’s build registration and login functionality in the login page.

Create a Signup page at /src/views/Signup.vue -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<template>
  <div style="justify: center">
    <div class="row">
      <div class="col-3-md" />
      <div class="col">
        <div class="card cardycard row is-center">
          <h2
            class="col-12"
            style="font-weight:bold; padding-bottom:1em; padding-top:1em;"
          >
            Sign up
          </h2>
          <div class="col-12">
            <label for="login">Email</label>
            <input name="login" v-model="email" />
          </div>
          <div class="col-12">
            <label for="password">Password</label>
            <input name="password" type="password" v-model="password" />
          </div>
          <div class="col-12">
            <label for="userid">User Id</label>
            <input name="userid" v-model="userid" />
          </div>
          <div class="col-12">
            <a class="button primary" @click.stop="register">Sign up</a>
          </div>
        </div>
      </div>
      <div class="col-3-md" />
    </div>
  </div>
</template>

<script>
  import { db } from "../db";
  import firebase from "firebase";

  import { mapState, mapMutations } from "vuex";

  export default {
    data() {
      return {
        email: "",
        password: "",
        userid: "",
        login: {},
      };
    },

    methods: {
      ...mapMutations(["setUser"]),
      async register() {
        try {
          const provider = new firebase.auth.GoogleAuthProvider();
          const user = await firebase
            .auth()
            .createUserWithEmailAndPassword(this.email, this.password);
          await user.user.updateProfile({
            displayName: this.userid,
          });
          console.log("user: ", user);

          this.setUser({
            email: user.user.email,
            name: user.user.displayName,
            uid: user.user.uid,
            refreshToken: user.user.refreshToken,
            displayName: user.user.displayName,
          });

          this.$toast.success("Signed up!");
          this.$router.push("/tweets");
        } catch (e) {
          console.error(e);
          this.$toast.error(e.message);
        }
      },
    },
  };
</script>

We collect user email, password and display name (no validation!), and passing it across to Firestore to create the user in our system.

You can start clicking around and see the users getting created provided you create blank vue files for all the other links outlined in App.vue.

Next create a new file called /src/views/Login.vue and add code to create a couple of fields -

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<template>
  <div style="justify: center">
    <div class="row">
      <div class="col-3-md" />
      <div class="col">
        <div class="card cardycard row is-center" v-if="!user.uid">
          <h2
            class="col-12"
            style="font-weight:bold; padding-bottom:1em; padding-top:1em;"
          >
            Login
          </h2>
          <div class="col-12">
            <label for="login">Email</label>
            <input name="login" v-model="email" />
          </div>
          <div class="col-12">
            <label for="password">Password</label>
            <input name="password" type="password" v-model="password" />
          </div>
          <div class="col-12">
            <a class="button primary" @click.stop="doLogin">Login</a>
          </div>
        </div>

        <div class="card cardycard row is-center" v-else>
          <div class="col-12">You are already logged in!</div>
          <div class="col-12">
            <a class="button primary" @click.stop="doLogout">Logout</a>
          </div>
        </div>
      </div>
      <div class="col-3-md" />
    </div>
  </div>
</template>

<script>
  import { db } from "../db";
  import firebase from "firebase";

  import { mapMutations, mapState } from "vuex";

  export default {
    data() {
      return {
        email: "",
        password: "",
        login: {},
      };
    },
    computed: {
      ...mapState(["user"]),
    },
    methods: {
      ...mapMutations(["setUser"]),
      async doLogout() {
        this.setUser({});
        await firebase.auth().signOut();
      },
      async doLogin() {
        try {
          const provider = new firebase.auth.GoogleAuthProvider();
          const { user } = await firebase
            .auth()
            .signInWithEmailAndPassword(this.email, this.password);

          this.setUser({
            email: user.email,
            name: user.displayName,
            uid: user.uid,
            refreshToken: user.refreshToken,
            displayName: user.displayName,
          });
          this.$toast.success("Logged in!");
          this.$router.push("/tweets");
        } catch (e) {
          this.$toast.error(e.message);
          console.error(e);
        }
      },
    },
  };
</script>

You should be able to login with users created through the signup page.

Let’s secure tweets on Firestore. Navigate to Firebase Console > Cloud Firestore. Click on Rules tab on main page. We will replace the default rules to allow only authenticated users to interact with our app.

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

     match /tweets/{tweet} {
      allow create:
      	if request.auth != null && request.auth.uid == request.resource.data.uid

    	allow update, delete:
      	if request.auth != null && request.auth.uid == resource.data.uid
      allow read:
      	if true
    }
  }

}

All we have done here is to -

  • enable authenticated users to read / create tweets
  • enable only owners to delete or update tweets

With a couple of clicks on the Firebase console, and some copy/paste magic, we have created a secure app with full email/password authentication (incl. confirmation email and everything)!

Start Tweeting

Create /src/views/Tweet.vue and include the below code -

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
<template>
  <div class="container">
    <h1 style="font-weight:bold;">Tweets</h1>
    <div class="row is-center">
      <div class="col-10 col-8-md">
        <form
          class="row is-right"
          @submit.prevent="postTweet()"
          v-if="user.uid"
        >
          <input
            name="tweetin"
            v-model="tweetin"
            placeholder="What's happening?"
            class="col-12"
          />
          <div class="col-2">
            <a
              class="button primary outline"
              type="submit"
              @click.prevent="postTweet()"
            >
              Tweet
            </a>
          </div>
        </form>
      </div>

      <div
        class="card tweetcard col-10 col-8-md"
        v-for="(tweet, index) in tweets"
      >
        <div class="row">
          <div class="col-6 is-left">@{{ tweet.uname }}</div>
          <div class="col-6 is-right" style="color: grey; font-size: 80%">
            {{ tweet.createdAt ? new Date(tweet.createdAt).toUTCString() : "" }}
          </div>
          <div class="col-12 is-left">{{ tweet.message }}</div>
          <div class="col-12 is-right">
            <a
              class="button icon clear"
              @click="deleteTweet(tweet.id)"
              v-if="tweet.uid == user.uid"
            >
              <img
                src="https://icongr.am/feather/trash-2.svg?size=16&amp;color=amber"
                alt="del"
              />
            </a>
            <a class="button icon clear" @click="postTweet(tweet.message)">
              <img
                src="https://icongr.am/feather/refresh-ccw.svg?size=16&amp;color=#e1e1e1"
                alt="rt"
              />
            </a>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
  import { db } from "../db";

  import { mapState } from "vuex";

  export default {
    data() {
      return {
        tweets: [],
        tweetin: "",
      };
    },
    computed: {
      ...mapState(["user"]),
    },
    mounted() {},
    methods: {
      async postTweet(twt) {
        await db.collection("tweets").add({
          message: `${twt ? twt : this.tweetin}`,
          uid: this.user.uid,
          uname: this.user.displayName,
          createdAt: Date.now(),
        });
        if (!twt) this.tweetin = "";
      },
      async deleteTweet(id) {
        try {
          const rec = await db.collection("tweets").doc(id).delete();
        } catch (e) {
          this.$toast.error(e.message);
        }
      },
    },
    firestore: {
      tweets: db.collection("tweets").orderBy("createdAt", "desc"),
    },
  };
</script>

And.. that’s it. You can celebrate and come back to do some testing whether your app indeed works.

Login with any user id, navigate to Tweets, and start tweeting. You can see others’ tweets and retweet them. You could also delete those covfefe tweets that seemed a good idea when you were in college.

The End

Hopefully you were entertained enough by this post? I always try to do my best with the jokes.. and of course, there was a bit of what Firestore could do.

The code for this project is available on Github.

Firestore is awesome, and Vue makes the entire developer experience delightful. Start weaving your magic with the combination and build great things.

Stay in touch!
Share on

Prashanth Krishnamurthy
WRITTEN BY
Prashanth Krishnamurthy
Technologist | Creator of Things


What's on this Page