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:
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.
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.
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.
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 |
Next, select the Settings tab within the Collections and update the permissions to manage users' access and rights.
Finally, register your web app. Click the Overview tab on the left pane and Add the web app platform.
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
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
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
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>
Next, in the entry point of the project, app.vue
, change the component NuxtWelcome
to NuxtPage
like this:
app.vue
<template>
<NuxtPage />
</template>
Here’s the result to confirm that the Appwrite CSS library is working:
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
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
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>
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>
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>
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>
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>
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>
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 directivev-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>
The code above is responsible for the following:
- The
getTodo
function is responsible for listing all of the todo items in the app within theonMounted
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>
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>
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.