This tutorial will walk you through building a simple CRUD (Create, Read, Update, Delete) application using Vue 3, Pinia for state management, and Firebase Firestore as the backend. This setup is perfect for building modern web apps requiring real-time data syncing, clean architecture, and easy state handling.
Prerequisites
Before we begin, make sure you have the following:
- Node.js and npm installed
- A Firebase account
- Basic knowledge of Vue 3 and Firebase
- Vue CLI or Vite installed globally (npm install -g create-vite)
1. Project Setup
First, create a new Vue 3 project using Vite:
npm create vite@latest vue3-pinia-firebase-crud -- --template vue
cd vue3-pinia-firebase-crud
npm install
npm run dev
Go to http://localhost:5173/ on your browser.
Press Ctrl + C to stop the running Vue application. Install the required dependencies:
npm install firebase pinia
Now, set up Pinia in your main entry file:
import { createApp } from "vue";
import "./style.css";
import { createPinia } from "pinia";
import App from "./App.vue";
const app = createApp(App);
app.use(createPinia());
app.mount("#app");
2. Firebase Configuration
1. Go to Firebase Console and create a new project.
2. Enable Firestore Database in the Build section -> Firebase Database -> Create Database -> choose Test Mode.
3. Click on the Web icon (</>), register your app, and copy the Firebase configuration.
Create a new file, src/firebase.js, for Firebase config:
import { initializeApp } from 'firebase/app'
import { getFirestore } from 'firebase/firestore'
const firebaseConfig = {
apiKey: "YOUR_API_KEY",
authDomain: "YOUR_AUTH_DOMAIN",
projectId: "YOUR_PROJECT_ID",
storageBucket: "YOUR_STORAGE_BUCKET",
messagingSenderId: "YOUR_MESSAGING_SENDER_ID",
appId: "YOUR_APP_ID"
}
const app = initializeApp(firebaseConfig)
const db = getFirestore(app)
export { db }
3. Creating a Pinia Store
Create a new folder src/stores and a file src/stores/todoStore.js for your store:
import { defineStore } from 'pinia'
import { collection, getDocs, addDoc, doc, deleteDoc, updateDoc } from 'firebase/firestore'
import { db } from '../firebase'
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: []
}),
actions: {
async fetchTodos() {
const querySnapshot = await getDocs(collection(db, 'todos'))
this.todos = querySnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }))
},
async addTodo(todo) {
await addDoc(collection(db, 'todos'), todo)
this.fetchTodos()
},
async deleteTodo(id) {
await deleteDoc(doc(db, 'todos', id))
this.fetchTodos()
},
async updateTodo(id, updated) {
await updateDoc(doc(db, 'todos', id), updated)
this.fetchTodos()
}
}
})
4. Building the UI
Let’s create simple components to handle adding, displaying, and editing todos.
Create a components/AddTodo.vue, then add:
<template>
<form @submit.prevent="handleAdd" class="add-form">
<input v-model="text" placeholder="New Todo" class="input" />
<button type="submit" class="button">Add</button>
</form>
</template>
<script setup>
import { ref } from 'vue'
import { useTodoStore } from '../stores/todoStore'
const text = ref('')
const store = useTodoStore()
function handleAdd() {
if (text.value.trim()) {
store.addTodo({ text: text.value })
text.value = ''
}
}
</script>
Create a components/TodoList.vue, then add:
<template>
<ul class="todo-list">
<li v-for="todo in store.todos" :key="todo.id" class="todo-item">
{{ todo.text }}
<button @click="store.deleteTodo(todo.id)" class="delete-button">Delete</button>
</li>
</ul>
</template>
<script setup>
import { onMounted } from 'vue'
import { useTodoStore } from '../stores/todoStore'
const store = useTodoStore()
onMounted(() => {
store.fetchTodos()
})
</script>
In src/App.vue, replace all:
<template>
<div class="app-container">
<h1 class="title">Vue 3 + Pinia + Firebase Todo App</h1>
<AddTodo />
<TodoList />
</div>
</template>
<script setup>
import AddTodo from './components/AddTodo.vue'
import TodoList from './components/TodoList.vue'
</script>
In src/style.css, replace with:
body {
font-family: Arial, sans-serif;
background: #f4f4f4;
margin: 0;
padding: 2rem;
}
.app-container {
max-width: 600px;
margin: auto;
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.title {
text-align: center;
margin-bottom: 1.5rem;
}
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 1.5rem;
}
.input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 5px;
}
.button {
background-color: #42b983;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 5px;
cursor: pointer;
}
.button:hover {
background-color: #369a6e;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-item {
display: flex;
justify-content: space-between;
background: #fafafa;
padding: 0.75rem 1rem;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 0.5rem;
}
.delete-button {
background-color: #e74c3c;
color: white;
border: none;
padding: 0.3rem 0.7rem;
border-radius: 4px;
cursor: pointer;
}
.delete-button:hover {
background-color: #c0392b;
}
Now, the Vue application looks like this.
5. Deploying to Firebase Hosting (Optional)
To deploy your Vue app to Firebase Hosting:
npm run build
firebase login
firebase init
Select Hosting and choose `dist` as the public directory.
firebase deploy
Conclusion
In this tutorial, you learned how to build a basic CRUD application using Vue 3, Pinia, and Firebase Firestore. This setup is perfect for small to medium-scale apps and can be easily extended with Firebase Authentication, Storage, and more advanced features.
You can find the full source code on our GitHub.
That's just the basics. If you need more deep learning about MEVN Stack, Vue.js,s or related, you can take the following cheap course:
- Isomorphic JavaScript with MEVN Stack
- Fun Projects with Vue 2
- Learning Path: Vue. js: Rapid Web Development with Vue 2
- Getting Started with Vue JS 2
- Vue. js 2 Recipes
Thanks!