How to use Appwrite cloud database in your Nuxt.js app

teri - May 26 '23 - - Dev Community

Appwrite is an open-source and secure backend-as-a-service platform with database functions and other core APIs necessary to build server-like applications for web, mobile, and Flutter developers. Appwrite integrates with both client and server-side programming languages.

Appwrite Cloud provides the same services as Appwrite, like functions, authentication, database, storage, etc. Using Appwrite Cloud, everything is managed directly from a dedicated URL instead of the local instance running on Docker.

This tutorial will show you how to build a create, read, update, and delete (CRUD) application in NuxtJS, making use of Appwrite Cloud and Pink Design suitable for frontend developers.

Project overview

At the end of this lesson, the CRUD application should look something like this:

Tooodooos app

GitHub and demo

Check the complete source code for this project in this repo. Also, try the demo here.

Prerequisites

The following are required to complete this tutorial:

  • An understanding of JavaScript, Vue, and CSS
  • Node >= 16 for dependencies installation
  • Access to an Appwrite Cloud account

Join and submit a request for Appwrite Cloud here.

Setting up Appwrite Cloud

Log in to Appwrite Cloud and create a new project in your cloud instance by clicking the + Create project button.

create project

PS: make sure to give the project a desired name.

Create a database

Navigate into the created project, click Databases on the window's left pane, and give your database a name.

TooodooosDB

Creating collections

Appwrite uses collections as containers of documents. By clicking on the database name created, click the Create collection button and give the Collections a name.

Collections

Adding attributes

It is vital to create field parameters, as they will hold all the registered data in the database. To create attributes, navigate to the created Collections and click the Attributes tab. For this tutorial, the attributes are as follows:

Attribute Key Attribute type Size Default value Required
todo String 255 - Yes

create attribute

Next, select the Settings tab within the Collections and update the permissions to manage users' access and rights.

Permissions

Finally, register your web app. Click the Overview tab on the left pane and Add the web app platform.

Register your web app

The Hostname with the asterisk (*) ensures access during development; otherwise, if not set, cross-origin resource sharing (CORS) may prevent access to the site data, thereby throwing errors.

Scaffolding a Nuxt app

Nuxt is a progressive open-source framework built on top of Vue. Let’s scaffold a new Nuxt application using the following command:



    npx nuxi init todo


Enter fullscreen mode Exit fullscreen mode

Follow the instructions and run the provided commands like yarn install. This command will install all the required dependencies.

Next, navigate to the project directory, then todos, and start the development server in the terminal:



    cd todos && yarn dev


Enter fullscreen mode Exit fullscreen mode

Nuxt app

Installing dependencies

As mentioned earlier, you need these two dependencies in the Nuxt app: Appwrite and Appwrite Pink Design.

In your terminal, run this command:



    yarn add appwrite
    yarn add @appwrite.io/pink


Enter fullscreen mode Exit fullscreen mode

Including Appwrite Pink in the project

Before you see the action of Appwrite CSS and its icons, let’s create a pages folder in the app's root directory. After that, create a file, index.vue, and add the following code.

pages/index.vue



    <template>
      <p class="heading-level-1">Add tooodooos</p>
    </template>

    <script setup lang="ts">
    import "@appwrite.io/pink";
    import "@appwrite.io/pink-icons";
    </script>


Enter fullscreen mode Exit fullscreen mode

Next, in the entry point of the project, app.vue, change the component NuxtWelcome to NuxtPage like this:

app.vue



    <template>
      <NuxtPage />
    </template>


Enter fullscreen mode Exit fullscreen mode

Here’s the result to confirm that the Appwrite CSS library is working:

Heading 1 class to text

The project tree directory should look something like this:



    .
    ├── pages
    │   └── index.vue
    ├── public
    ├── package.json
    ├── tsconfig.json
    ├── yarn.lock
    ├── README.md
    ├── nuxt.config.js
    └── app.vue


Enter fullscreen mode Exit fullscreen mode

Building the UI

The user interface for the todos app will showcase all the todos created using an input field and list all of them with an edit and a delete icon.

Now, let’s update the app directory with a new directory called components, which will include the following files: AboutTodo.vue, Header.vue, ListTodo.vue, and Todos.vue. Also, create another file within the pages folder called about.vue.

The updated project tree directory:



    .
    ├── components
    │   ├── AboutTodo.vue
    │   ├── Header.vue
    │   ├── ListTodo.vue
    │   └── Todos.vue
    ├── pages
    │   ├── about.vue
    │   └── index.vue
    ├── public
    ├── package.json
    ├── tsconfig.json
    ├── yarn.lock
    ├── README.md
    ├── nuxt.config.js
    └── app.vue


Enter fullscreen mode Exit fullscreen mode

Include the following code in the file components:

components/AboutTodo.vue



    <template>
      <div class="container">
        <h2 class="eyebrow-heading-2">About Tooodooos</h2>
        <p class="text" :style="{ 'margin-top': 1 + 'rem' }">
          Using Appwrite functions and Appwrite Cloud, adding todos have become
          simpler for anyone wanting to create their own. Appwrite as a tool is a
          backend-as-a-service platform.
        </p>
        <p class="text" :style="{ 'margin-top': 1 + 'rem' }">
          The technology used to build this app is Nuxt, Appwrite Pink for the
          design system, and integrating Appwrite of course.
        </p>
      </div>
    </template>


Enter fullscreen mode Exit fullscreen mode

The AboutTodo component will display the info about the project on navigating to the /about page when clicked in the navigation bar.

The code above centers the content of the About page using Appwrite Pink Design. Also included within this component are Vue style bindings in the <p> element which sets a top margin of 1rem.

components/Header.vue



    <template>
      <header
        class="u-flex u-main-space-between u-cross-center u-position-sticky"
        style="--inset-block-start: auto">
        <NuxtLink to="/" class="u-bold">Tooodooos</NuxtLink>
        <ul class="list">
          <li class="list-item">
            <span class="text">
              <NuxtLink to="/about">About</NuxtLink>
            </span>
          </li>
        </ul>
      </header>
    </template>


Enter fullscreen mode Exit fullscreen mode

The code snippet above uses Appwrite’s Pink Display and List element to style the navigation bar. The NuxtLink component is also included for navigation to the home and about pages, respectively.

components/ListTodo.vue



    <template>
      <div class="u-flex u-main-space-between u-cross-center u-width-full-line">
        <span class="text">{{ item }}</span>
        <div class="u-cursor-pointer">
          <span
            class="icon-pencil"
            aria-hidden="true"
            :style="{ 'margin-right': space + 'em' }"></span>
          <span class="icon-trash" aria-hidden="true"></span>
        </div>
      </div>
    </template>

    <script setup>
    const props = defineProps({
      item: String,
    });

    const space = ref("1");
    </script>


Enter fullscreen mode Exit fullscreen mode

The code above displays the todo list item from the props array and the pencil and trash icon from Pink Design.

components/Todos.vue



    <template>
      <div class="container">
        <Header />
        <h1 class="heading-level-1" :style="{ 'margin-top': 1 + 'rem' }">
          {{ name }}
        </h1>
        <form
          class="form u-width-full-line u-max-width-500 u-flex u-main-center"
          :style="{ 'margin-top': 1 + 'em' }">
          <ul class="form-list">
            <li class="form-item">
              <label class="label">Todo</label>
              <div class="input-text-wrapper">
                <input
                  class="input-text"
                  type="text"
                  placeholder="add new todo"
                  v-model="input.todo" />
              </div>
            </li>
          </ul>
          <button class="button" :style="{ 'margin-top': 1 + 'em' }">
            <span class="text">Add todo</span>
          </button>
        </form>
        <div :style="{ 'margin-top': marginTop + 'em' }">
          <ul class="list">
            <li class="list-item">
              <list-todo item="Create API documentation" />
            </li>
          </ul>
        </div>
      </div>
    </template>

    <script setup>
    const name = ref("Add tooodooos");

    const marginTop = ref("3");

    const input = reactive({
      todo: "",
    });
    </script>


Enter fullscreen mode Exit fullscreen mode

The following occurs in the code snippet above:

  • In the script section, declare the variables using the composition API
  • Pass these values in the <template>
  • Import the Header and list-todo components
  • Used the classes from Pink Design and defined :style bindings to the elements
  • Pass the item props to the list-todo component

pages/about.vue



    <template>
      <div class="container">
        <Header />
        <about-todo />
      </div>
    </template>


Enter fullscreen mode Exit fullscreen mode

The code is responsible for importing the Header and about-todo components to display the content of the about page.

Creating environment variables

As the deployed project is publicly available on GitHub, creating a local file, .env, that will include all your secret keys and constants is advisable.

Check out this guide on creating environment variables in a Nuxt.js app.

Posting a new todo to Appwrite Cloud

The create operation in create, read, update, and delete (CRUD) will use the HTTP protocol method, POST, which could mean creating a new list, task, or post. This action will send this request to the database Appwrite Cloud.

Now, update the Todos component with the following code:

components/Todos.vue



    <template>
      <div class="container">
        <Header />
        <h1 class="heading-level-1" :style="{ 'margin-top': 1 + 'rem' }">
          {{ name }}
        </h1>
        <form
          class="form u-width-full-line u-max-width-500 u-flex u-main-center"
          :style="{ 'margin-top': 1 + 'em' }"
          @submit.prevent="handleInputChange">
          <ul class="form-list">
            <li class="form-item">
              <label class="label">Todo</label>
              <div class="input-text-wrapper">
                <input
                  class="input-text"
                  type="text"
                  :placeholder="inputError ? 'please enter a todo' : 'add new todo'"
                  v-model="input.todo" />
              </div>
            </li>
          </ul>
          <button class="button" :style="{ 'margin-top': 1 + 'em' }">
            <span class="text">Add todo</span>
          </button>
        </form>
        <div :style="{ 'margin-top': marginTop + 'em' }">
          <ul class="list">
            <li class="list-item">
              <list-todo item="Create API documentation" />
            </li>
          </ul>
        </div>
      </div>
    </template>

    <script setup>
    import { Client, Databases, ID } from "appwrite";

    const client = new Client();
    const databases = new Databases(client);

    const runtimeConfig = useRuntimeConfig();

    client
      .setEndpoint(runtimeConfig.public.API_ENDPOINT)
      .setProject(runtimeConfig.public.PROJECT_ID);

    const name = ref("Add tooodooos");

    const marginTop = ref("3");

    const input = reactive({
      todo: "",
    });

    const inputError = ref(false);

    const create = (data) =>
      databases.createDocument(
        runtimeConfig.public.DATABASE_ID,
        runtimeConfig.public.COLLECTION_ID,
        ID.unique(),
        data
      );

    const handleInputChange = () => {
      if (!input.todo) {
        return (inputError.value = true);
      }
      create({
        todo: input.todo,
      }).then(
        function (response) {
          window.location.reload();
        },
        function (error) {
          console.log(error);
        }
      );
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

The following occurs in this code snippet:

  • Import the Appwrite package and initialize a new instance of the web SDK
  • Using the useRuntimeConfig(), you can access the environment variables
  • The inputError variable set to false is helpful for error checking when the user tries to send an empty input field by binding the :placeholder attribute
  • The create function with the parameter data is to connect the attribute key, todo in Appwrite Cloud with the value passed in the <input> element using the directive v-model
  • The function handleInputChange passed to the <form> using the @submit.prevent directive will send the typed value in the input field to the Appwrite server
  • With every new todo added, the pages refresh with the window.location.reload() function

Displaying all the todos list

This section shows the list of todos from the server onto the application's client-side using the read operation.

Again, update the Todos component with this code:

components/Todos.vue



    <template>
      <div class="container">
        <Header />
        <h1 class="heading-level-1" :style="{ 'margin-top': 1 + 'rem' }">
          {{ name }}
        </h1>
        <form
          class="form u-width-full-line u-max-width-500 u-flex u-main-center"
          :style="{ 'margin-top': 1 + 'em' }"
          @submit.prevent="handleInputChange">
          <ul class="form-list">
            <li class="form-item">
              <label class="label">Todo</label>
              <div class="input-text-wrapper">
                <input
                  class="input-text"
                  type="text"
                  :placeholder="inputError ? 'please enter a todo' : 'add new todo'"
                  v-model="input.todo" />
              </div>
            </li>
          </ul>
          <button class="button" :style="{ 'margin-top': 1 + 'em' }">
            <span class="text">Add todo</span>
          </button>
        </form>
        <div :style="{ 'margin-top': marginTop + 'em' }">
          <ul class="list">
            <li class="list-item" v-for="item in todos" :key="item.$id">
              <list-todo :item="item" />
            </li>
          </ul>
        </div>
      </div>
    </template>
    <script setup>
    ...

    const todos = ref(null);

    const getTodo = databases.listDocuments(
      runtimeConfig.public.DATABASE_ID,
      runtimeConfig.public.COLLECTION_ID
    );

    onMounted(() => {
      getTodo.then(
        function (response) {
          todos.value = response.documents;
        },
        function (error) {
          console.log(error);
        }
      );
    });
    </script>


Enter fullscreen mode Exit fullscreen mode

The code above is responsible for the following:

  • The getTodo function is responsible for listing all of the todo items in the app within the onMounted lifecycle hook
  • Looping through the array using the v-for directive on the
  • element and replacing the previous item props with the v-bind:item or :item attribute on the list-todo component

Updating a todo item

Correcting your list items (todos) is crucial for any CRUD app using the UPDATE operation. Copy-paste this updated code:

components/ListTodo.vue



    <template>
      <div class="u-flex u-main-space-between u-cross-center u-width-full-line">
        <span class="text">{{ item.todo }}</span>
        <div class="u-cursor-pointer">
          <span
            class="icon-pencil"
            aria-hidden="true"
            @click.prevent="showModal = !showModal"
            :style="{ 'margin-right': space + 'em' }"></span>
          <span class="icon-trash" aria-hidden="true"></span>
        </div>
      </div>
      <div
        v-if="showModal"
        class="u-z-index-20 u-padding-24"
        :style="{
          position: 'fixed',
          top: '0',
          right: 0,
          left: 0,
          bottom: 0,
          'background-color': 'rgba(0, 0, 0, 0.8)',
          height: '100vh',
        }">
        <form
          class="form u-width-full-line u-max-width-500 u-flex u-main-center"
          @submit.prevent="handleUpdateTodo">
          <ul class="form-list">
            <li class="form-item">
              <label class="label" :style="{ color: 'white' }">Edit todo</label>
              <div class="input-text-wrapper">
                <input
                  class="input-text"
                  type="text"
                  v-model="item.todo" />
              </div>
            </li>
          </ul>
          <button class="button" :style="{ 'margin-top': 1 + 'em' }">
            <span class="text">Update todo</span>
          </button>
        </form>
        <div class="u-cursor-pointer">
          <span
            class="icon-x u-font-size-32"
            @click.prevent="showModal = !showModal"
            aria-hidden="true"
            :style="{
              position: 'absolute',
              top: '0',
              right: '1em',
              color: '#fff',
            }"></span>
        </div>
      </div>
    </template>

    <script setup>
    import { Client, Databases } from "appwrite";

    const client = new Client();
    const databases = new Databases(client);

    const runtimeConfig = useRuntimeConfig();

    client
      .setEndpoint(runtimeConfig.public.API_ENDPOINT)
      .setProject(runtimeConfig.public.PROJECT_ID);

    const props = defineProps({
      item: Object,
    });

    const space = ref("1");

    const showModal = ref(false);

    const updateTodo = (database_id, collection_id, document_id, data) =>
      databases.updateDocument(database_id, collection_id, document_id, data);

    const handleUpdateTodo = () => {
      updateTodo(props.item.$databaseId, props.item.$collectionId, props.item.$id, {
        todo: props.item.todo,
      }).then(
        function (response) {
          console.log(`${props.item.todo} successfully updated in DB`);
        },
        function (error) {
          console.log("Error updating the document", error.message);
        }
      );
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

This code snippet aims to update a document with its unique ID. Using the patch method, it updates only the partial data:

  • The variable showModal populates the modal with a click on the pencil icon
  • Within the handleUpdateTodo function, passing the object with a key todo and the value passed in the to show the exact data using the directive, v-model

Deleting a todo list

The last operation for this CRUD app is to use the delete method to delete the data from the client-side and the server.

Let’s update the ListTodo component with this code:

components/ListTodo.vue



    <template>
      <div class="u-flex u-main-space-between u-cross-center u-width-full-line">
        <span class="text">{{ item.todo }}</span>
        <div class="u-cursor-pointer">
          <span
            class="icon-pencil"
            aria-hidden="true"
            @click.prevent="showModal = !showModal"
            :style="{ 'margin-right': space + 'em' }"></span>
          <span
            class="icon-trash"
            aria-hidden="true"
            @click.prevent="handleDeleteTodo"></span>
        </div>
      </div>
      <div
        v-if="showModal"
        class="u-z-index-20 u-padding-24"
        :style="{
          position: 'fixed',
          top: '0',
          right: 0,
          left: 0,
          bottom: 0,
          'background-color': 'rgba(0, 0, 0, 0.8)',
          height: '100vh',
        }">
        <form
          class="form u-width-full-line u-max-width-500 u-flex u-main-center"
          @submit.prevent="handleUpdateTodo">
          <ul class="form-list">
            <li class="form-item">
              <label class="label" :style="{ color: 'white' }">Edit todo</label>
              <div class="input-text-wrapper">
                <input class="input-text" type="text" v-model="item.todo" />
              </div>
            </li>
          </ul>
          <button class="button" :style="{ 'margin-top': 1 + 'em' }">
            <span class="text">Update todo</span>
          </button>
        </form>
        <div class="u-cursor-pointer">
          <span
            class="icon-x u-font-size-32"
            @click.prevent="showModal = !showModal"
            aria-hidden="true"
            :style="{
              position: 'absolute',
              top: '0',
              right: '1em',
              color: '#fff',
            }"></span>
        </div>
      </div>
    </template>

    <script setup>
    ...

    const deleteTodo = (database_id, collection_id, document_id) =>
      databases.deleteDocument(database_id, collection_id, document_id);

    const handleDeleteTodo = () => {
      deleteTodo(
        props.item.$databaseId,
        props.item.$collectionId,
        props.item.$id
      ).then(
        function (response) {
          window.location.reload();
        },
        function (error) {
          console.log(error);
        }
      );
    };
    </script>


Enter fullscreen mode Exit fullscreen mode

The handleDeleteTodo function deletes a todo item with a unique ID from both the client-side and the server with the @click event on the <span> delete icon.

At this point, the UI will look like this:

Conclusion

This tutorial showed you how to build a todo CRUD application and pair its functionalities with Appwrite Cloud to create, store, update, and delete data from the client and server.

Resources

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .