Learn how to create a Nuxt application.

CRUD – Create, Read, Update, Delete

I assume that you already know the fundamentals of Vue JS and/or you are a bit familiar with the framework. Nuxt JS is a robust framework, built on Vue JS. It is essentially the same as Vue JS. Then why, Nuxt?

For most people, the decision to use Nuxt JS is usually for its SSR capabilities.

What is SSR?

SSR is an abbreviation for Server Side Rendering.

Usually, for most Single Page Applications (SPA), rendered files are auto injected into the DOM after the page has loaded. Hence, bots, SEO crawlers will find an empty page on page load. However, for SSR, due to its ability to pre-render apps on the server before page, that page can be easily indexed by SEO crawlers. Also, it possibly makes the app even more performant than a regular SPA.

Nuxt JS gives developers the ability to create SSR applications with ease. Regular Vue JS SPA apps can also be configured to use SSR, but the process is somewhat cumbersome, and Nuxt JS provides a wrapper to handle all of that configuration. Asides the SSR, Nuxt also provides an easy way to set up your VueJS project with more efficiency.

Although Nuxt JS is still Vue JS, it has some fundamental differences in how its folder architecture is structured.

The focus of this article is for you to be able to build an app with Nuxt; hence, we aren’t going to dive deep into Nuxt’s folder architecture, however, I will quickly explain some of the important ones we might require here.

Pages

The pages folder is one of the fundamental differences from regular Vue SPA. It represents the Views folder in regular Vue architecture, mores o, in Nuxt, files created in the Pages folder are automatically provisioned as a route. Meaning, when you create an index.vue file in the pages folder, that automatically becomes your root route, i.e., localhost:3000/.

Also, when you create any other filename.vue, it becomes a route โ€” creating about.vue allows you access localhost:3000/about.

You can also create a folder within the Pages folder. If you create a folder named ‘contact’ and within that folder, you have email.vue, then you can access localhost:3000/contact/email. It is that simple. This way, you don’t need to manually create a router.js file as you would typically do with Vue JS to create your routes.

Components

It’s still pretty much the same as with Vue JS, components created are not automatically provisioned as routes.

Static

Static folder replaces the public folder in regular Vue JS apps, functions pretty much the same. Files here do not get compiled; they are served the same way they are stored.

You can read all about the architecture and structure at the Nuxt JS documentation page.

Now, Let’s build something interesting…

Building a book store app

We will be building a book store app, where a user can add books they have read to a particular category they like. It will look like this.

So, we will have a simple layout as above, just 3 columns containing the different sections of books. Recently read books, favorite books, and yeah, best of the best books (I ‘ll confess, I didn’t know what to call that section, ๐Ÿ™‚ )

So the goal here, is to be able to add a book’s title, author and description to a card on any of the sections, edit already added books and remove an existing book. We will not be utilizing any database, so everything happens in the state.

First, we install Nuxt:

npm install create-nuxt-app

Second, after installing Nuxt, you can now create the project with the command,

create-nuxt-app bookStore

I choose to name my app ‘bookStore’; you can name your something cooler ^_^

Then, let’s walk through the remaining prompts, enter a description,

Author name, type a name or press enter to retain defaults

Select a package manager, whichever you are comfortable with, both are fine

Select a UI framework. For this project, I will be using Vuetify, then again, any UI framework you are comfortable with will do just fine.

Select a custom server framework; we don’t need any, I ‘ll select none

Extra modules, select what you want, or select both, we wouldn’t be using them for this app.

Linting is important. Let’s go with ESLint.

While testing is important, we will not be looking at that today, so none

Rendering mode, yeah SSR it is.

Note: Choosing SSR doesn’t mean we don’t get the benefit of having a SPA, the app still remains a SPA but with SSR. The other option means simply SPA and no SSR.

Hit enter and move on,

And our project is creating,

After creation, we can now go into the directory and run

yarn dev

if you are using npm as your package manager, use,

npm run dev

By default, the app runs at localhost:3000. Visit the link in your browser, and you should see a default Nuxt page.

Now let’s begin with creating components we need. We will have cards displaying each book information, and we will have a modal containing a form to enter new book information or edit existing ones.

To create a component, simply create a new file in the components folder. Here is the code for my card component.

// BookCard.vue

<template>
  <v-card class="mx-auto" max-width="400">
    <v-img src="https://cdn.vuetifyjs.com/images/cards/sunshine.jpg" height="200px"></v-img>
    <v-card-title>{{bookTitle}}</v-card-title>
    <v-card-subtitle>{{bookAuthor}}</v-card-subtitle>
    <v-card-text>{{bookDescription}}</v-card-text>
    <v-card-actions>
      <v-spacer></v-spacer>
      <slot name="button"></slot>
    </v-card-actions>
  </v-card>
</template>

<script>
export default {
  props: ["bookTitle", "bookAuthor", "bookDescription"]
};
</script>

A quick explanation of what is done above. The image is hardcoded; we will not bother about that for now. The book title, book author, and book description are passed down to this component from the parent page as props. If you are not familiar with props, imagine them as entry points through with this component can be populated with data.

Now to the next component, the modal.

//BookModal.vue

<template>
  <v-dialog max-width="500px" v-model="open">
    <v-card class="p-5">
      <v-card-title>Add Books</v-card-title>
      <v-form>
        <v-select v-model="category" :items="categories" label="Select A Category"></v-select>
        <v-text-field v-model="title" label="Enter Book Title"></v-text-field>
        <v-text-field v-model="author" label="Enter Book Author"></v-text-field>
        <v-textarea v-model="description" label="Enter Book Description"></v-textarea>
      </v-form>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn @click.stop="saveBook" color="green">Add</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

Now, that is the markup for the modal; we need to create the v-models as data properties; therefore, we will add a script tag below the <template> tag.

<script>
export default {
  data() {
    return {
      category: "",
      title: "",
      author: "",
      description: "",
    };
  },
}
</script>

Also, there is a ‘Select a Category’ dropdown that is expecting ‘categories’ data. We will add that to the data.

<script>
export default {
  data() {
    return {
      open: false,
      category: "",
      title: "",
      author: "",
      description: "",
      categories: ["Recently read books", "Favourite books", "Best of the best"]
    };
  },
}
</script>

Now, we need a way to toggle our modal open and close, for now, we will just have an ‘open’ data property as above. We will look closely at that next.

Let’s quickly create our view page where we will have three grids/columns, one for each section of the book. Let’s call the page index.vue, see the code below.

//index.vue
<template>
  <div>
    <v-row>
      <v-col md="4">
        <h2 class="text-center mb-5">Recently Read Books</h2>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Favourite Books</h2>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Best of the Best</h2>
      </v-col>
    </v-row>
    <BookModal />
  </div>
</template>

Now that we have our grids, we need to add our card component to each grid, for every book added. Therefore, we will import our BookCard.vue component.

<template>
  <div>
    <v-row>
      <v-col md="4">
        <h2 class="text-center mb-5">Recently Read Books</h2>
        <v-row v-for="(item,index) in recentBooks" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Favourite Books</h2>
        <v-row v-for="(item,index) in favouriteBooks" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Best of the Best</h2>
        <v-row v-for="(item,index) in bestOfTheBest" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
    </v-row>
  </div>
</template>

Now, we have imported the BookCard component and have bound its props to results from the loop; this ensures that for every entry added to any of the sections, there is a card created for it. Also, on each card, we will include buttons to edit or remove a card.

Now, we need to import the card from the script and define the arrays that will be holding records for each of the categories.

<script>
import BookCard from "@/components/BookCard";

export default {
  components: {
    BookCard,
  },
  data() {
    return {
      recentBooks: [],
      favouriteBooks: [],
      bestOfTheBest: []
    };
  },
};
</script>

Next, we need to have a button in the header that will open up the modal whenever we need to add books. We will do this in the ‘default.vue’ file. We will add the button to the default app bar header.

<v-btn color="green" @click.stop="openModal">Add Books</v-btn>

Next, we need a create the openModal method in the script section. In regular Vue JS apps, there is an event bus that allows you communicate with another component and even pass data across, in Nuxt JS, there still an event bus and you can still create it the same way. So, we will use an event bus to pass data open a modal in the index.vue page (which we are yet to import) from the layout/default.vue file.

Let’s see how it is done.

To create a global event bus, open a file in the root directory of the project, name it eventBus.js and paste the code below in it.

import Vue from 'vue'

export const eventBus = new Vue()

Yeah, that all. Now we can use it.

<script>
import { eventBus } from "@/eventBus";
methods: {
    openModal() {
      eventBus.$emit("open-add-book-modal");
    }
  }
</script>

Next, we will go back to our the BookModal component, and listen to when the eventBus emits ‘open-add-book-modal’. We will add this to the script section.

import { eventBus } from "@/eventBus";

created() {
    eventBus.$on("open-add-book-modal", this.open = true);
  },

Now, we can open and close our modal, but it doesn’t add any books yet. Let’s add a method to our Modal to make it save what is added to the state (remember we are not making use of any database or local storage). We add this next to ‘created()’

methods: {
    saveBook() {
      let cardData = {
        title: this.title,
        author: this.author,
        description: this.description,
        category: this.category
      };
      eventBus.$emit("save-book", cardData);
      this.open = false;
    }
  }

Next, we need a way to re-populate the modal when we are editing data from any of the cards. So let’s make some adjustments to the ‘created()’

created() {
    eventBus.$on("open-add-book-modal", data => {
      if (data) {
        this.category = data.category;
        this.title = data.title;
        this.author = data.author;
        this.description = data.description;
      }
      this.open = true;
    });
  },

Now, the BookModal looks like this as a whole,

//BookModal.vue


<template>
  <v-dialog max-width="500px" v-model="open">
    <v-card class="p-5">
      <v-card-title>Add Books</v-card-title>
      <v-form>
        <v-select v-model="category" :items="categories" label="Select A Category"></v-select>
        <v-text-field v-model="title" label="Enter Book Title"></v-text-field>
        <v-text-field v-model="author" label="Enter Book Author"></v-text-field>
        <v-textarea v-model="description" label="Enter Book Description"></v-textarea>
      </v-form>
      <v-card-actions>
        <v-spacer></v-spacer>
        <v-btn @click.stop="saveBook" color="green">Add</v-btn>
      </v-card-actions>
    </v-card>
  </v-dialog>
</template>

<script>
import { eventBus } from "@/eventBus";
export default {
  data() {
    return {
      open: false,
      category: "",
      title: "",
      author: "",
      description: "",
      categories: ["Recently read books", "Favourite books", "Best of the best"]
    };
  },
  created() {
    eventBus.$on("open-add-book-modal", data => {
      if (data) {
        this.category = data.category;
        this.title = data.title;
        this.author = data.author;
        this.description = data.description;
      }
      this.open = true;
    });
  },
  methods: {
    saveBook() {
      let cardData = {
        title: this.title,
        author: this.author,
        description: this.description,
        category: this.category
      };
      eventBus.$emit("save-book", cardData);
      this.open = false;
    }
  }
};
</script>

Next, we can now go back to the index.vue page to import the BookModal component. We will add this to the script section.

<script>
import BookCard from "@/components/BookCard";
import BookModal from "@/components/BookModal";
import { eventBus } from "@/eventBus";

export default {
  components: {
    BookCard,
    BookModal
  },
  data() {
    return {
      recentBooks: [],
      favouriteBooks: [],
      bestOfTheBest: []
    };
  },
</script>

Also, in the body, we will add,

<BookModal/>

We need methods for edit and remove a card. In the template earlier, I already passed the edit and remove methods to the buttons as shown below, likewise, I passed in the arguments required for each method.

<template v-slot:button> <v-btn @click.stop="edit(item,index)">Edit</v-btn> <v-btn @click.stop="remove(item.category, index)">Remove</v-btn> </template>

Let’s create the methods.

methods: {
    remove(category, index) {
      if (category === "Recently read books") {
        this.recentBooks.splice(index, 1);
      }
      if (category === "Favourite books") {
        this.favouriteBooks.splice(index, 1);
      }
      if (category === "Best of the best") {
        this.bestOfTheBest.splice(index, 1);
      }
    },
    edit(item, index) {
      if (item.category === "Recently read books") {
        eventBus.$emit("open-add-book-modal", item);
        this.recentBooks.splice(index, 1);
      }
      if (item.category === "Favourite books") {
        eventBus.$emit("open-add-book-modal", item);
        this.favouriteBooks.splice(index, 1);
      }
      if (item.category === "Best of the best") {
        eventBus.$emit("open-add-book-modal", item);
        this.bestOfTheBest.splice(index, 1);
      }
    }
  }

Remember, the BookModal is emitting, and an event called save-book, we need a listener for that event here.

created() {
    eventBus.$on("save-book", cardData => {
      if (cardData.category === "Recently read books") {
        this.recentBooks.push(cardData);
      }
      if (cardData.category === "Favourite books") {
        this.favouriteBooks.push(cardData);
      }
      if (cardData.category === "Best of the best") {
        this.bestOfTheBest.push(cardData);
      }
    });
  },

Now, in one whole look, our index.vue page looks like this

<template>
  <div>
    <v-row>
      <v-col md="4">
        <h2 class="text-center mb-5">Recently Read Books</h2>
        <v-row v-for="(item,index) in recentBooks" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <nuxt-link :to="`/books/${item.title}`">
                <v-btn>View</v-btn>
              </nuxt-link>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Favourite Books</h2>
        <v-row v-for="(item,index) in favouriteBooks" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
      <v-col md="4">
        <h2 class="text-center mb-5">Best of the Best</h2>
        <v-row v-for="(item,index) in bestOfTheBest" :key="index">
          <BookCard
            class="mb-5"
            :bookTitle="item.title"
            :bookAuthor="item.author"
            :bookDescription="item.description"
          >
            <template v-slot:button>
              <v-btn @click.stop="edit(item,index)">Edit</v-btn>
              <v-btn @click.stop="remove(item.category, index)">Remove</v-btn>
            </template>
          </BookCard>
        </v-row>
      </v-col>
    </v-row>
    <BookModal />
  </div>
</template>

<script>
import BookCard from "@/components/BookCard";
import BookModal from "@/components/BookModal";
import { eventBus } from "@/eventBus";

export default {
  components: {
    BookCard,
    BookModal
  },
  data() {
    return {
      recentBooks: [],
      favouriteBooks: [],
      bestOfTheBest: []
    };
  },
  created() {
    eventBus.$on("save-book", cardData => {
      if (cardData.category === "Recently read books") {
        this.recentBooks.push(cardData);
        this.recentBooks.sort((a, b) => b - a);
      }
      if (cardData.category === "Favourite books") {
        this.favouriteBooks.push(cardData);
        this.favouriteBooks.sort((a, b) => b - a);
      }
      if (cardData.category === "Best of the best") {
        this.bestOfTheBest.push(cardData);
        this.bestOfTheBest.sort((a, b) => b - a);
      }
    });
  },
  methods: {
    remove(category, index) {
      if (category === "Recently read books") {
        this.recentBooks.splice(index, 1);
      }
      if (category === "Favourite books") {
        this.favouriteBooks.splice(index, 1);
      }
      if (category === "Best of the best") {
        this.bestOfTheBest.splice(index, 1);
      }
    },
    edit(item, index) {
      if (item.category === "Recently read books") {
        eventBus.$emit("open-add-book-modal", item);
        this.recentBooks.splice(index, 1);
      }
      if (item.category === "Favourite books") {
        eventBus.$emit("open-add-book-modal", item);
        this.favouriteBooks.splice(index, 1);
      }
      if (item.category === "Best of the best") {
        eventBus.$emit("open-add-book-modal", item);
        this.bestOfTheBest.splice(index, 1);
      }
    }
  }
};
</script>

If you got this far, Great Job!!! You are Awesome!

As earlier mentioned, every .vue file created in the pages folder is automatically provisioned as a route, likewise, for every folder created within the pages folder. This doesn’t only hold for static pages, and dynamic pages can be created this way too!

Let’s see how.

Using our current project, let’s say we want to add a dynamic page for all the book cards with a view button to view more details about a book.

Let’s quickly add a view button, and use a <nuxt-link> to visit the page. Yeah, <nuxt-link> replaces <router-link> and it works.

<nuxt-link :to="`/${item.title}`">
                <v-btn>View</v-btn>
              </nuxt-link>

Next, we create a dynamic folder by prefixing the name with an underscore. i.e., _title and inside that folder, we will have an index.vue file that gets rendered when we visit that route.

Just for demonstration, we will only be accessing the params property within the file.

// _title/index.vue

<template>
  <h1>{{$route.params.title}}</h1>
</template>

Now, when we click view, it opens another page where we can see the title we have passed through the route. This can be developed to do anything we want as far as dynamic pages are concerned.

That’s it for this lesson!

The complete code for this can be found in this repository. You are welcome to contribute to the code. If you are interested in mastering the framework, then I would suggest this Udemy course.