Skip to content

How to Build a Vue Todo App with Bazaar

In this tutorial, you will build a simple Todo application using Vue.js and Bazaar. This guide will walk you through setting up a new Vue project, integrating Bazaar for backend services, and deploying your application to Netlify. By the end of this tutorial, you will have a fully functional Todo app with features to add, display, mark as complete, and delete tasks.

Prerequisites

To complete this tutorial, you will need:

  • A basic understanding of Vue 3.
  • Node.js installed on your machine.
  • Git installed on your machine.
  • A GitHub account for code repository hosting.
  • (Optional) A Netlify account for deploying your app.

Step 1 — Scaffolding a New Vue Project

In this step, you will create a new Vue project using the official Vue project scaffolding tool. The created project will be using a build setup based on Vite.

Open up your terminal and ensure your current working directory is where you intend to create a project. Then, Run the following command:

Terminal window
npm create vue@latest

That command will install and execute create-vue, the official Vue project scaffolding tool.

Choose the project name, bazaar-vue-todo. Then, answer the feature prompts as follows:

Terminal window
Add TypeScript? Yes
Add JSX Support? No
Add Vue Router for Single Page Application development? No
Add Pinia for state management? No
Add Vitest for Unit Testing? No
Add an End-to-End Testing Solution? No
Add ESLint for code quality? No
Add Vue DevTools 7 extension for debugging? No

These choices will set up a minimal Vue project with TypeScript support, which is sufficient for this tutorial.

Next, navigate to your project directory, install dependences, and start the development server:

Terminal window
cd bazaar-vue-todo
npm install
npm run dev

Open the default URL http://localhost:5173/ in your browser to see the starter Vue project.

In the next step, you’ll prepare your project by cleaning up unnecessary scaffolded files and adding custom styles.

Step 2 — Preparing the Project

In this step, you will clean up the scaffolded Vue project by removing unnecessary files and adding custom styles.

  1. First, delete the following files and folders:

    • src/components/*
    • src/assets/base.css
    • src/assets/logo.svg
  2. Replace the contents of src/assets/main.css with:

    main.css
    body {
    font-family: sans-serif;
    }
    .container {
    margin: 0 auto;
    max-width: 600px;
    padding: 3em 0;
    }
    .card {
    background-color: #eee;
    padding: 1rem;
    }
    .todo-form {
    margin: 2rem 0;
    }
    .todos {
    display: flex;
    flex-direction: column;
    gap: 1rem;
    }
    .todo-text {
    margin-bottom: 1rem;
    }
    .strikethrough {
    text-decoration: line-through;
    }
  3. Finally, replace the contents of src/App.vue with the following:

    App.vue
    <script setup lang="ts">
    // Todo logic will be implemented here
    </script>
    <template>
    <div class="container">
    <h1>Todo</h1>
    <!-- Todo template content will be implemented here -->
    </div>
    </template>
    <style scoped></style>

Check your app in the browser to see the text Todo.

With the project cleaned up, you are ready to integrate Bazaar for authentication and realtime data storage.

Step 3 — Running the Mock Server

In this step, you will use the Bazaar mock server to develop your app locally. The mock server simulates the Bazaar backend, allowing you to test and build your application in a local environment.

In a new terminal window, run the following command to start the mock server:

Terminal window
npx @bzr/bazaar-mock -u http://localhost:5173/

This command assumes the Vite dev server is running on the default port 5173. Update accordingly if you’re running on a different port.

Install the @bzr/bazaar-mock NPM package if prompted.

You should see output similar to this:

Terminal window
Running mock Bazaar server at: http://localhost:3377
App ID is test
App URL is http://localhost:5173/
Redirect URIs are http://localhost:5173/
Use any email to sign up / log in. Code is always 1234

Now that the mock server is running, you’ll initialize a Bazaar client.

Step 4 — Initializing a Bazaar Client

In this step, you will install and initialize the Bazaar client to manage backend services for your Todo app.

First, install the Bazaar library:

Terminal window
npm install @bzr/bazaar

Next, initialize the Bazaar client in src/App.vue:

App.vue
<script setup lang="ts">
// Todo logic will be implemented here
import { BazaarApp } from "@bzr/bazaar";
const bzr = new BazaarApp({
appId: import.meta.env.VITE_APP_ID || "test",
loginRedirectUri: import.meta.env.VITE_URL || window.location.href,
onApiConnectError: async (bzr) => bzr.logOut(),
});
// Test the client initialization by checking the logged in status
console.log("is logged in:", bzr.isLoggedIn());
</script>

In this code, you set the following BazaarApp options:

  • appId: Checks for a VITE_APP_ID environment variable and, if not present, uses test, the Bazaar mock server default.
  • loginRedirectUri: Checks for a VITE_URL environment variable and, if not present, uses window.location.href as the default, e.g. http://localhost:5173/.
  • onApiConnectError: If a connection error occurs, logs out the user.

Open up the app in in your browser and check the developer console. You’ll see the following:

Developer Console
is logged in: false

With the Bazaar client initialized you can now add authentication to your app.

Step 5 — Adding User Authentication

In this step, you’ll add user authentication to your app using Bazaar’s OAuth2-based authentication system.

First, add log in and log out buttons to your template:

App.vue
<template>
<div class="container">
<h1>Todo</h1>
<!-- Todo template content will be implemented here -->
<button @click="bzr.login">Log in or create account</button>
<button @click="bzr.logOut">Log out</button>
</div>
</template>

When a user clicks the log in button, a pop-up window opens, prompting them to create a Bazaar account or log in if they already have one. Once the account is created, the user is prompted to authorize your Todo app.

Create a test account by entering any email address, using the code 1234 and choosing any handle and name. Then, authorize the app. The pop-up will close. Refresh the page, and in your developer console you’ll see the following:

Developer Console
is logged in: true

Next, refactor the code so the login status updates immediately after a successful login, not just on page load.

App.vue
<script setup lang="ts">
...
// Test the client initialization by checking the logged in status
console.log("is logged in:", bzr.isLoggedIn());
async function doLoginActions() {
console.log("is logged in:", bzr.isLoggedIn());
}
// Perform login actions if the user is already logged in on page load
if (bzr.isLoggedIn()) {
doLoginActions();
}
// Set up handler to perform login actions immediately after a successful login
bzr.onLogin(doLoginActions);
</script>

Log out and log back in, and you’ll see is logged in: true in your console after the pop-up closes without needing a page refresh.

You have now added user authentication to your app. Next, you will conditionally display content based on the user’s login status.

Step 6 — Displaying Logged-in Content

In this step, you will conditionally display content based on the user’s login status.

First, make the following changes to your script:

App.vue
<script setup lang="ts">
import { BazaarApp } from "@bzr/bazaar";
import { ref } from "vue";
const bzr = new BazaarApp({ ... });
const isLoggedIn = ref(false);
async function doLoginActions() {
console.log("is logged in:", bzr.isLoggedIn());
isLoggedIn.value = true;
}
...
</script>

In these changes, you create a reactive variable for the login status and set its value to true when a user logs in.

Next, update the template to display log in and log out buttons conditionally:

App.vue
<template>
<div class="container">
<h1>Todo</h1>
<template v-if="!isLoggedIn">
<button @click="bzr.login">Log in or create account</button>
</template>
<template v-else>
<button @click="bzr.logOut">Log out</button>
</template>
</div>
</template>

The app now displays the appropriate button based on the user’s login status.

Next, you’ll add the functionality to create todos.

Step 7 — Adding a Todo

In this step, you’ll implement the feature to create todos.

Make the following changes to your script:

App.vue
<script setup lang="ts">
import { BazaarApp, type SubscribeListener } from "@bzr/bazaar";
import { ref, type Ref } from "vue";
const bzr = new BazaarApp({ ... });
const isLoggedIn = ref(false);
type Todo = {
id: string;
text: string;
completed: boolean;
}
const todoText = ref("");
const todos: Ref<Todo[]> = ref([]);
const todosCollection = bzr.collection<Todo>("todos");
async function onSubmit() {
const unpersistedTodo = { text: todoText.value, completed: false };
const id = await todosCollection.insertOne(unpersistedTodo);
todos.value.push({ id, ...unpersistedTodo });
todoText.value = "";
}
...
</script>

In this code, you define a Todo type and create two reactive variables, one for the new todo text and another for holding the list of todos.

The bzr.collection method creates an object to interact with a database collection. Learn more in the collections docs.

The onSubmit function adds a new todo to the todos collection with todosCollection.insertOne (collections are created lazily) and adds the todo to your app’s local state with the ID generated by the database after insertion.

Next, add the form to your template after the log out button:

App.vue
<template>
...
<button @click="bzr.logOut">Log out</button>
<form class="todo-form" @submit.prevent="onSubmit">
<input
type="text"
v-model="todoText"
placeholder="Write a todo..."
required
/>
<button>Add</button>
</form>
...
</template>

In these changes, you hook up the onSubmit handler to the form submit event and bind the todoText reactive variable you just created to the input.

You have now implemented the functionality to add new todos, but they still need to be displayed.

In the next step, you will display these todos in your app.

Step 8 — Displaying Todos

In this step, you will fetch and display todos from the collection.

First, update the doLoginActions function to fetch todos:

App.vue
<script setup lang="ts">
...
async function doLoginActions() {
isLoggedIn.value = true;
todosCollection.getAll().then((docs) => (todos.value = docs));
}
...
</script>

In this code, todosCollection.getAll() fetches all the todos from the collection, and the results are stored in the todos reactive variable.

Next, update the template to display the todos:

App.vue
<template>
...
<form class="todo-form" @submit.prevent="onSubmit">...</form>
<div class="todos">
<div class="card" v-for="todo in todos" :key="todo.id">
<div class="todo-text">
{{ todo.text }}
</div>
</div>
</div>
...
</template>

Add a few more todos in your browser and notice they get added to the bottom of your list. Usually you want the newest todos to appear first. To do that, you’ll reverse the fetched todos.

App.vue
<script setup lang="ts">
...
import { ref, computed, type Ref } from "vue";
const todos: Ref<Todo[]> = ref([]);
const fetchedTodos: Ref<Todo[]> = ref([]);
const todos = computed(() => [...fetchedTodos.value].reverse());
const todosCollection = bzr.collection<Todo>("todos");
async function onSubmit() {
const unpersistedTodo = { text: todoText.value, completed: false };
const id = await todosCollection.insertOne(unpersistedTodo);
todos.value.push({ id, ...unpersistedTodo });
fetchedTodos.value.push({ id, ...unpersistedTodo });
todoText.value = "";
}
async function doLoginActions() {
isLoggedIn.value = true;
todosCollection.getAll().then((docs) => (todos.value = docs));
todosCollection.getAll().then((docs) => (fetchedTodos.value = docs));
}
...
</script>

In these changes, you store fetched todos in a new reactive variable, fetchedTodos, and update todos to be a computed property that will updated when fetchedTodos changes. The spread operator (...) creates a new array, as the reverse method updates an array in place. Doing fetchedTodos.value.reverse() would cause an infinite re-render.

You have now successfully displayed the todos fetched from the collection. In the next step, you will add realtime syncing to keep the todos updated across different devices.

Step 9 — Adding Realtime Syncing

In this step, you will enable realtime updates for your todos so that changes are reflected instantly across different devices.

The todos reactive variable is an array so you can use the arrayMirrorSubscribeListener helper. Import it:

App.vue
import { BazaarApp, arrayMirrorSubscribeListener } from "@bzr/bazaar";

Then, update the onSubmit and doLoginActions methods as follows:

App.vue
<script setup lang="ts">
...
function onSubmit() {
const unpersistedTodo = { text: todoText.value, completed: false };
todosCollection.insertOne(unpersistedTodo);
todoText.value = "";
}
async function doLoginActions() {
isLoggedIn.value = true;
todosCollection.subscribeAll({}, arrayMirrorSubscribeListener(fetchedTodos.value));
}
...
</script>

In this code, todosCollection.subscribeAll subscribes to all changes in the todos collection, and arrayMirrorSubscribeListener handles change events, ensuring the local todos array stays in sync with the collection. As such, in onSubmit, you no longer need to add the todo to your local state. Learn about subscribe listeners.

To test the realtime updates, open your app in a private window while keeping the original window open, and log in. When you make changes in one window, you’ll see them appear in the other in realtime.

With realtime syncing enabled, any changes to the todos will be updated instantly. In the next step, you will implement the functionality to toggle the completion status of todos.

Step 10 — Toggling Todo Completion

In this step, you’ll implement functionality to toggle the completion status of todos.

First, add the toggleTodoCompletion function:

App.vue
<script setup lang="ts">
...
function onSubmit() { ... }
function toggleTodoCompletion(todo: Todo) {
todosCollection.updateOne(todo.id, { completed: !todo.completed });
}
...
</script>

That function updates the completed value for a todo, setting it to the opposite of its current state. Remember, you don’t need to update your local state. The arrayMirrorSubscribeListener helper keeps your local state in sync with the database.

Next, update the template to include a button for toggling the completion status and conditionally style the todo text:

App.vue
<template>
...
<div class="todos">
<div class="card" v-for="todo in todos" :key="todo.id">
<div class="todo-text" :class="{ strikethrough: todo.completed }">
{{ todo.text }}
</div>
<button @click="toggleTodoCompletion(todo)">
Mark as {{ todo.completed ? "Incomplete" : "Completed" }}
</button>
</div>
</div>
...
</template>

In this template, the strikethrough class is applied to the todo text if the todo is marked as completed, and a button is added to toggle the completion status.

You can now mark todos as completed or incomplete. In the next step, you will add the functionality to delete todos.

Step 11 — Deleting a Todo

In this step, you will implement the functionality to delete todos.

First, add the deleteTodo function:

App.vue
<script setup lang="ts">
...
function toggleTodoCompletion(todo: Todo) { ... }
function deleteTodo(id: string) {
if (!window.confirm("Delete todo forever?")) return;
todosCollection.deleteOne(id);
}
...
</script>

Here, window.confirm prompts the user confirmation before deleting. Again, arrayMirrorSubscribeListener will handle the onDelete event and update your local state.

Next, update the template to include a button for deleting todos:

App.vue
<template>
...
<button @click="toggleTodoCompletion(todo)">
Mark as {{ todo.completed ? "Incomplete" : "Completed" }}
</button>
<button @click="deleteTodo(todo.id)">Delete</button>
...
</template>

That code change will render a delete button for each todo.

Show the Completed App.vue File
App.vue
<script setup lang="ts">
import { BazaarApp, arrayMirrorSubscribeListener } from "@bzr/bazaar";
import { ref, computed, type Ref } from "vue";
const bzr = new BazaarApp({
appId: import.meta.env.VITE_APP_ID || "test",
loginRedirectUri: import.meta.env.VITE_URL || window.location.href,
onApiConnectError: async (bzr) => bzr.logOut(),
});
const isLoggedIn = ref(false);
type Todo = {
id: string;
text: string;
completed: boolean;
};
const todoText = ref("");
const fetchedTodos: Ref<Todo[]> = ref([]);
const todos = computed(() => [...fetchedTodos.value].reverse());
const todosCollection = bzr.collection<Todo>("todos");
function onSubmit() {
const unpersistedTodo = { text: todoText.value, completed: false };
todosCollection.insertOne(unpersistedTodo);
todoText.value = "";
}
function toggleTodoCompletion(todo: Todo) {
todosCollection.updateOne(todo.id, { completed: !todo.completed });
}
function deleteTodo(id: string) {
if (!window.confirm("Delete todo forever?")) return;
todosCollection.deleteOne(id);
}
async function doLoginActions() {
isLoggedIn.value = true;
todosCollection.subscribeAll({}, arrayMirrorSubscribeListener(fetchedTodos.value));
}
// Perform login actions if the user is already logged in on page load
if (bzr.isLoggedIn()) {
doLoginActions();
}
// Set up handler to perform login actions immediately after a successful login
bzr.onLogin(doLoginActions);
</script>
<template>
<div class="container">
<h1>Todo</h1>
<template v-if="!isLoggedIn">
<button @click="bzr.login">Log in or create account</button>
</template>
<template v-else>
<button @click="bzr.logOut">Log out</button>
<form class="todo-form" @submit.prevent="onSubmit">
<input
type="text"
v-model="todoText"
placeholder="Write a todo..."
required
/>
<button>Add</button>
</form>
<div class="todos">
<div class="card" v-for="todo in todos" :key="todo.id">
<div class="todo-text" :class="{ strikethrough: todo.completed }">
{{ todo.text }}
</div>
<button @click="toggleTodoCompletion(todo)">
Mark as {{ todo.completed ? "Incomplete" : "Completed" }}
</button>
<button @click="deleteTodo(todo.id)">Delete</button>
</div>
</div>
</template>
</div>
</template>
<style scoped></style>

With that, your todo app is complete and ready to deploy.

Step 12 — Deploying to Netlify

In this step, you will deploy your Vue.js Todo app to Netlify. You can deploy a Bazaar-powered app anywhere static sites can be hosted. Netlify provides a frictionless deployment process and free static site hosting for public GitHub repos.

First, initialize a Git repository and push your code to GitHub:

Terminal window
git init
git add .
git commit -m "Initial commit: Vue todo app"

Go to GitHub and create a public repo named bazaar-vue-todo.

Terminal window
git remote add origin https://github.com/<your-username>/bazaar-vue-todo.git
git branch -M main
git push -u origin main

Next, go to Netlify, create an account if necessary, and click Add new site. Select Import an existing project and choose GitHub. Select your repository bazaar-vue-todo. If you can’t find your repo, click Configure the Netlify app on GitHub to grant access.

Configure the site settings and build settings as follows:

  • Site name: e.g. bazaar-vue-todo
  • Branch to deploy: e.g. main
  • Build command: npm run build-only (default)
  • Publish directory: dist (default)

Add environment variables VITE_URL and VITE_APP_ID. For VITE_URL, use your app’s Netlify URL (e.g., https://bazaar-vue-todo.netlify.app).

To get your Bazaar app ID, open cloud.bzr.dev and create a free account. Click on the user icon at the top right, then click Developers. Next, click Create App.

Give your app a name. I’m using Vue Todo. Set your APP URL, which should be the same as your app’s Netlify URL, e.g. https://bazaar-vue-todo.netlify.app. Finally, click Create, then copy your app ID. It will look something like 01907291-d4e5-79d5-a312-ba20260c9b98.

Return to Netlify and paste in your app ID as the VITE_APP_ID value, and click Deploy. You’ll see a notice, Site deploy in progress. The process will take a few minutes, after which your todo app will be deployed!

Conclusion

In this tutorial, you built and deployed a Todo app using Vue.js and Bazaar. You set up a new Vue project, integrated Bazaar for backend services, implemented user authentication, added and displayed todos, enabled realtime syncing, and deployed your app to Netlify.

Check the Bazaar documentation for more features and capabilities.