Create a Recipe App with Tailwind and Nuxt.js
Tags: nuxt, appwrite, tailwindcss, vue
Published: 2023-01-18 09:53:12

---
title: Create a Recipe App with Tailwind and Nuxt.js
published: true
description: Learn how to create a recipe app in Nuxt.js using TailwindCSS and Appwrite.
tags: #nuxt #appwrite #tailwindcss #vue
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/snlry6i12l5uor6o8ori.png
# Use a ratio of 100:42 for best results.
# published_at: 2022-11-25 00:44 +0000
---

According to [Verified Market Research](https://www.verifiedmarketresearch.com/product/recipe-apps-market/), the recipe apps market size is projected to reach USD 1,098.22 Million by 2028. They are designed with intent and excellent user experience to make cooking easier, faster, and more accessible.

In this post, we will learn how to create a recipe app in Nuxt.js using TailwindCSS and Appwrite. The project’s GitHub repository can be found [here](https://github.com/Mr-Malomz/recipe-app).

## Prerequisites

To fully grasp the concepts presented in this tutorial, the following requirements apply:

- Basic understanding of JavaScript and Vue.js
- Docker [installation](https://www.docker.com/get-started/)
- An Appwrite instance; check out this [article](https://dev.to/hackmamba/create-a-local-appwrite-instance-in-3-steps-19n9?utm_source=hackmamba&utm_medium=hackmamba) on how to set up an instance



## Getting Started

We need to create a Nuxt.js starter project by navigating to the desired directory and running the command below in our terminal.

```bash
npx nuxi init recipe-app && cd recipe-app
```

The command creates a Nuxt.js project called `recipe-app` and navigates into the project directory.

Next, we need to install Nuxt.js dependencies by running the command below in our terminal.

```bash
npm install
```

## Installing dependencies

**Installing TailwindCSS**
[TailwindCSS](https://tailwindcss.com/) is a utility-first CSS framework packed with classes to help us style our web page. To use it in our application, run the command below in our terminal.

```bash
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

The command installs TailwindCSS and its dependencies and generates a `tailwind.config.js` file.

Next, we need to update the `tailwind.config.js` file with the snippet below:

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
    content: [
        './components/**/*.{js,vue,ts}',
        './layouts/**/*.vue',
        './pages/**/*.vue',
        './**/*.vue',
        './plugins/**/*.{js,ts}',
    ],
    theme: {
        extend: {},
    },
    plugins: [],
};
```

Next, we need to add TailwindCSS directives to our application. The directives give our application access to TailwindCSS utility classes. To do this, create a `css/tailwind.css` file in the root directory and add the snippet below:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

Lastly, we need to add TailwindCSS as a dependency in the `nuxt.config.js`, as shown below:

```js
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
    postcss: {
        plugins: {
            tailwindcss: {},
            autoprefixer: {},
        },
    },
    css: ['~/css/tailwind.css'],
});
```

**Installing Appwrite**
[Appwrite](https://appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog) is a development platform that provides a powerful API and management console for building backend servers for web and mobile applications. To install it, run the command below:

```bash
npm install appwrite
```


## Setting up Appwrite

To get started, we need to log into our Appwrite console, click the **Create project** button, input `recipe_app` as the name, and then click **Create**.

![create project](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672866342209_Screenshot+2023-01-04+at+22.05.19.png)


**Create a Database, Collection, and Add Attributes**
With our project created, we can set up our application database. First, navigate to the **Database** tab, click the **Create database** button, input `recipes` as the name, and then click **Create**.

![Create database](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672866988797_Screenshot+2023-01-04+at+22.16.17.png)


Secondly, we need to create a collection for storing our recipes. To do this, click the **Create collection** button, input `recipe_collection` as the name, and then click **Create**.

![Create collection](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672867425130_Screenshot+2023-01-04+at+22.23.36.png)


Thirdly, we need to create attributes to represent our database fields. To do this, we need to navigate to the Attributes tab and create Attributes for each of the values shown below:

| Attribute key | Attribute type | Size | Required |
| ------------- | -------------- | ---- | -------- |
| title         | String         | 250  | YES      |
| ingredients   | String         | 5000 | YES      |
| direction     | String         | 5000 | YES      |

![Create attribute](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672867711596_Screenshot+2023-01-03+at+20.36.35.png)
![List of attributes](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1673167691966_Screenshot+2023-01-08+at+09.46.52.png)


Lastly, we need to update our database permission to manage them accordingly. To do this, we need to navigate to the **Settings** tab, scroll to the **Update Permissions** section, select `Any`, mark accordingly, and then click **Update**.

![select any](https://paper-attachments.dropboxusercontent.com/s_1F2738109066A0A5582E754A3908A7FD06BE17296D17A867AFE208C1CA00E9C0_1671384434901_Screenshot+2022-12-18+at+02.24.27.png)
![mark permission](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672867805095_Screenshot+2023-01-03+at+21.32.10.png)



# Building the Recipe App

To get started, first, we need to create a `components/utils.js` to abstract the application logic from the UI and add the snippet below:

```js
import { Client, Databases, Account, ID } from 'appwrite';

const PROJECT_ID = 'REPLACE WITH PROJECT ID';
const DATABASE_ID = 'REPLACE WITH DATABASE ID';
const COLLECTION_ID = 'REPLACE WITH COLLECTION ID';

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

client.setEndpoint('http://localhost/v1').setProject(PROJECT_ID);

export const account = new Account(client);

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

export const getList = () =>
    databases.listDocuments(DATABASE_ID, COLLECTION_ID);
```


The snippet above does the following:

- Imports the required dependency
- Initializes Appwrite `client` and `databases` with required arguments
- Creates `account`, `create`, and `getList` functions for managing user sessions, creating a recipe, and getting the list of recipes

**PS**: *We can get the required IDs on our Appwrite Console.*

Secondly, we need to create a `modal.vue` component inside the `components` folder for creating a recipe and add the snippet below:

`modal``.vue` Logic

```vue
<template>
<!-- markup goes here -->
</template>

<script setup>
import { create } from "./utils";
const props = defineProps(["onModalChange"]);

//state
const isLoading = ref(false);
const isError = ref(false);

const form = reactive({
  title: "",
  ingredients: "",
  direction: "",
});

const onSubmit = () => {
  isLoading.value = true;
  isError.value = false;
  create({
    title: form.title,
    ingredients: form.ingredients,
    direction: form.direction,
  })
    .then((res) => {
      isLoading.value = false;
      isError.value = false;
      props.onModalChange(false);
      window.location.reload();
    })
    .catch((_) => {
      isLoading.value = false;
      isError.value = true;
    });
};
</script>
```

The snippet above does the following:

- Imports required dependency and defines required props
- Creates application state and form elements
- Creates an `onSubmit` method that uses the `create` helper function to create recipes and update states accordingly

`modal.vue` UI

 ```vue  
<template>
  <div
    class="
      h-screen
      w-screen
      bg-cyan-900 bg-opacity-30
      z-30
      top-0
      fixed
      transform
      scale-105
      transition-all
      ease-in-out
      duration-100
    "
  >
    <div
      class="flex flex-col justify-center items-center h-full w-full open-nav"
    >
      <section
        class="
          w-11/12
          lg:w-1/2
          2xl:w-6/12
          bg-white
          flex
          justify-center
          items-center
          mt-5
          rounded-lg
        "
      >
        <div class="w-11/12 py-8">
          <div v-if="isError" class="text-center text-red-700">
            Error creating recipe
          </div>
          <div class="flex justify-between items-center mb-4">
            <h2 class="capitalize text-xl text-gray-500 font-medium">
              create recipe
            </h2>
            <button
              class="text-xs text-gray-700 hover:bg-gray-400"
              @click="props.onModalChange(false)"
            >
              Close
            </button>
          </div>
          <form @submit.prevent="onSubmit">
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block">Title</label>
              <input
                type="text"
                name="title"
                required
                placeholder="title"
                class="w-full h-10 border border-gray-400 rounded-sm px-4"
                v-model="form.title"
              />
            </fieldset>
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block"
                >Ingredients</label
              >
              <textarea
                rows="3"
                name="ingredients"
                required
                placeholder="rice, beans, ...."
                class="w-full border border-gray-400 rounded-sm px-4"
                v-model="form.ingredients"
              />
            </fieldset>
            <fieldset class="mb-4">
              <label class="text-sm text-gray-400 mb-2 block">Direction</label>
              <textarea
                rows="3"
                name="direction"
                required
                placeholder="details..."
                class="w-full border border-gray-400 rounded-sm px-4"
                v-model="form.direction"
              />
            </fieldset>
            <button
              class="
                text-white
                capitalize
                px-6
                py-2
                bg-cyan-900
                rounded-md
                w-full
              "
              :disabled="isLoading"
            >
              save
            </button>
          </form>
        </div>
      </section>
    </div>
  </div>
</template>

<script setup>
// app logic goes here
</script>
```

The snippet above uses the declared state to show error conditionally and bind form elements.

Lastly, we need to update the `app.vue` file as shown below:

`app.vue` Logic

```vue
<template>
<!-- markup goes here -->
</template>

<script setup>
import { getList, account } from "./components/utils";

//state
const isLoading = ref(false);
const isError = ref(false);
const isModal = ref(false);
const recipes = ref(null);

const onModalChange = (state) => {
  isModal.value = state;
};

onMounted(async () => {
  isLoading.value = true;
  isError.value = false;
  //check session
  account
    .get()
    .then()
    .catch((_) => account.createAnonymousSession());
  //make api call
  getList()
    .then((res) => {
      recipes.value = res;
      isLoading.value = false;
      isError.value = false;
    })
    .catch((_) => {
      isLoading.value = false;
      isError.value = true;
    });
});
</script>
```

The snippet above does the following:

- Imports the required dependency
- Creates application states and an `onModalChange` method to manage the visibility
- Conditionally checks if a user has a valid session using the `account` helper function and gets the list of recipes using the `getList` helper function

`app.vue` UI

```vue

<template>
  <div>
    <nav class="h-16 border-b flex justify-between items-center px-3 mb-10">
      <h2 class="font-bold text-xl">Recipe app</h2>
      <button
        class="
          text-sm
          font-bold
          bg-cyan-900
          hover:bg-cyan-700
          text-white
          rounded
          px-5
          py-2
        "
        @click="onModalChange(true)"
      >
        Create
      </button>
    </nav>
    <section class="flex justify-center">
      <div v-if="isLoading" class="w-full lg:w-2/4">Loading ...</div>
      <div v-if="isError" class="w-full lg:w-2/4 text-red-700">
        Error Loading Recipes
      </div>
      <ul v-if="recipes" class="w-full lg:w-2/4">
        <li
          v-for="recipe in recipes.documents"
          :key="recipe.$id"
          class="px-4 py-2 border rounded-lg mb-5"
        >
          <div class="border-b h-8 mb-4">
            <h3 class="text-gray-700 font-bold">{{ recipe.title }}</h3>
          </div>
          <div class="mb-4">
            <p class="text-xs text-gray-500 mb-2">Ingredients:</p>
            <p class="text-xs ml-2">
              {{ recipe.ingredients }}
            </p>
          </div>
          <div class="bg-cyan-50 p-4 rounded">
            <p class="text-xs text-gray-700 mb-2">Instruction:</p>
            <p class="text-xs text-gray-700 ml-2">
              {{ recipe.directions }}
            </p>
          </div>
        </li>
      </ul>
    </section>
  </div>
  <Modal v-if="isModal" :onModalChange="onModalChange" />
</template>

<script setup>
// app logic goes here
</script>
```

The snippet above does the following:

- Conditionally shows the list of recipes
- Uses the `Modal` component and passes in the required props


With that done, we can start a development server using the command below:

```bash
npm run dev
```


![Create recipe](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672870743784_Screenshot+2023-01-04+at+00.50.55.png)
![List of recipe](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672870743763_Screenshot+2023-01-04+at+00.50.48.png)


We can validate the saved recipes by checking our collection on Appwrite.

![Recipe collection](https://paper-attachments.dropboxusercontent.com/s_342E9FE96DE03D24E8AD672738F224A92FC913F6592531FAD506815CB3E2A539_1672870877297_Screenshot+2023-01-04+at+23.21.07.png)


 
 **P****.****S****.****:** *This demo is a base implementation to demonstrate Appwrite's support for scaffolding a working prototype. Appwrite gives developers the magic wound to extend the application to include multiple fields, separate collections, authentication, etc.*

# Conclusion

This post discussed creating a recipe app in Nuxt.js using TailwindCSS and Appwrite. The [Appwrite](https://appwrite.io/?utm_source=hackmamba&utm_medium=hackmamba-blog) platform allows developers the required experience and SDKs to build medium to large enterprise applications.

These resources might be helpful:

- [Appwrite official documentation](https://appwrite.io/docs?utm_source=hackmamba&utm_medium=hackmamba-blog)
- [Nuxt 3 official documentation](https://nuxt.com/docs)
- [Appwrite web SDK](https://github.com/appwrite/sdk-for-web)