Introduction
Nuxt.js is a progressive web application (PWA) framework that builds on top of Vue.js to create server-rendered Vue applications. It provides an organized, optimized development structure out of the box along with advanced features like asynchronous data loading, middleware, and route transitions.
In this comprehensive guide, we will build a CRUD (Create, Read, Update, Delete) book store application from scratch using Nuxt.js and Vuetify.
Prerequisites
Before starting, you should have a basic understanding of:
- Vue.js fundamentals like components, props, data binding etc.
- JavaScript fundamentals like variables, functions, arrays, objects etc.
- ES6 syntax and features
- Node.js and NPM basics
Project Overview
We will build a book store app that allows users to add books to three categories:
- Recently Read
- Favorites
- Best of the Best
For each book, we will store the title, author, description and category.
The app will have the following features:
- Display books grouped under their respective categories
- Add a new book
- Edit an existing book
- Delete a book
- View book details
We will use Vuex to manage state and Vuetify as the UI framework.
Scaffolding the Nuxt App
Let‘s start by scaffolding out the basic Nuxt app structure using the create-nuxt-app
command:
npx create-nuxt-app bookstore
Choose the following options when prompted:
- Project name:
bookstore
- Programming language:
JavaScript
- Package manager:
Npm
- UI framework:
Vuetify.js
- Nuxt.js modules:
Axios
- Linting tools:
ESLint
- Testing framework:
None
- Rendering mode:
Universal (SSR)
This will generate the Nuxt project structure with the options we selected.
Installing Dependencies
Now install the project dependencies using:
cd bookstore
npm install
Project Structure
The main project folders and files generated are:
pages
– Contains the application views and routescomponents
– Contains reusable Vue componentslayouts
– Layout templates that wrap pagesstore
– Handles state management with Vuexmiddleware
– Handles custom middlewareplugins
– Plugins to inject dependencies into Vuenuxt.config.js
– Nuxt configuration file
Creating Layout
Open layouts/default.vue
and update it to following:
<template>
<v-app>
<v-navigation-drawer v-model="drawer" app>
<!-- navigation links -->
</v-navigation-drawer>
<v-app-bar
app
color="indigo"
dark
>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>My BookStore</v-toolbar-title>
<v-spacer></v-spacer>
<v-btn text @click="openAddBookDialog">
<v-icon left>mdi-plus</v-icon>
Add Book
</v-btn>
</v-app-bar>
<v-main>
<v-container>
<Nuxt />
</v-container>
</v-main>
<add-book-dialog/>
</v-app>
</template>
<script>
import AddBookDialog from ‘@/components/AddBookDialog‘
export default {
components: {
AddBookDialog
},
data () {
return {
drawer: false
}
},
methods: {
openAddBookDialog () {
this.$store.commit(‘openAddBook‘)
}
}
}
</script>
This sets up the basic page layout with the header, navigation drawer and page content section to display the active Nuxt page or route.
We add the <add-book-dialog>
component which we‘ll build later to handle adding books.
The openAddBookDialog
method will open this dialog by committing a state change using Vuex.
Registering Vuex Store
Create a file called store/index.js
and add the following code:
export const state = () => ({
addBookDialog: false
})
export const mutations = {
openAddBook(state) {
state.addBookDialog = true;
},
closeAddBook(state) {
state.addBookDialog = false
}
}
This creates the Vuex store with an addBookDialog
state that tracks whether the add book dialog box is open or closed.
The mutations allow committing changes to update this state.
Now open nuxt.config.js
and register the Vuex store module:
modules: [
‘@/store‘
],
Creating Book Model
Create a file model/book.js
with a basic book model:
export default class Book {
constructor(title, author, description, category) {
this.title = title
this.author = author
this.description = description
this.category = category
}
}
This exports a Book class that we instantiate and use later.
Building the UI Components
Under components
, create the following files:
BookCard.vue
<template>
<v-card>
<v-card-title>{{ book.title }}</v-card-title>
<v-card-text>
<div>Author: {{ book.author }}</div>
<div>Category: {{ book.category }}</div>
<v-spacer></v-spacer>
<div class="text-truncate">{{ book.description }}</div>
</v-card-text>
<v-card-actions>
<v-btn text>
View Details
</v-btn>
<v-spacer></v-spacer>
<v-btn icon @click="editBook">
<v-icon>mdi-pencil</v-icon>
</v-btn>
<v-btn icon @click="deleteBook">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
props: {
book: {
type: Object,
required: true
}
},
methods: {
editBook() {
this.$emit(‘edit‘, this.book)
},
deleteBook() {
this.$emit(‘delete‘, this.book)
}
}
}
</script>
This component displays the book details passed via a book
prop. It has edit and delete buttons which emit events for the parent component to handle.
AddBookDialog.vue
<template>
<v-dialog
v-model="open"
max-width="600"
>
.. dialog content
</v-dialog>
</template>
<script>
export default {
computed: {
open: {
get() {
return this.$store.state.addBookDialog
},
set(value) {
if(!value) {
this.$store.commit(‘closeAddBook‘)
}
}
}
},
}
</script>
This component renders the add book dialog conditionally based on the Vuex addBookDialog
state. When closed, it commits the state change.
Building the Pages
Under pages
, create an index.vue
file that handles the book listings:
<template>
<v-row>
<v-col
v-for="category in categories"
:key="category.name"
cols="12"
md="4"
>
<h2 class="text-center">{{ category.name }}</h2>
<book-card
v-for="(book, index) in category.books"
:key="index"
:book="book"
@edit="editBook"
@delete="deleteBook"
/>
<v-btn
color="primary"
@click="addBook(category.name)"
>
Add New Book
</v-btn>
</v-col>
</v-row>
</template>
<script>
import BookCard from ‘@/components/BookCard‘
export default {
components: {
BookCard
},
data() {
return {
categories: [
{
name: ‘Recently Read‘,
books: []
},
{
name: ‘Favorites‘,
books: []
},
{
name: ‘Best Books‘,
books: []
}
]
}
},
methods: {
addBook(category) {
// open add book dialog
// update state on save
},
editBook(book) {
// open add book dialog
// populate with book data
},
deleteBook(book) {
// delete book
}
}
}
</script>
This page has a grid that displays books under their categories.
The BookCard
component is used to display each book‘s data.
Placeholder methods are added for now to handle CRUD operations which we‘ll implement soon.
Under pages
, create a `_slug.vue
file to handle the book details page:
<template>
<v-card>
<v-card-title>{{ book.title }}</v-card-title>
<v-card-text>
<div>Author: {{ book.author }}</div>
<div>Category: {{ book.category }}</div>
<v-spacer></v-spacer>
<div>{{ book.description }}</div>
</v-card-text>
</v-card>
</template>
<script>
export default {
async asyncData ({ params }) {
// fetch book data using slug
const book = {}
return { book }
}
}
</script>
This makes an async call to fetch the book data dynamically based on route params.
With the core UI components and pages built out, let‘s work on the functionality.
Implementing CRUD Functionality
Adding Books
In the Vuex store, add some mock data:
export const state = () => ({
books: [
{
name: ‘Atomic Habits‘,
author: ‘James Clear‘,
category: ‘Recently Read‘,
description: ‘Methods to build good habits and break bad ones‘
},
{
name: ‘The Great Gatsby‘,
author: ‘F. Scott Fitzgerald‘,
category: ‘Favorites‘,
description: ‘The story primarily concerns the young and..."
}
]
})
Update openAddBook
mutation:
openAddBook(state) {
state.addBookDialog = true
state.selectedBook = {
name: ‘‘,
author: ‘‘,
description: ‘‘,
category: ‘‘
}
}
Now update AddBookDialog.vue
:
<template>
<v-form @submit="addBook">
<!-- form controls to get book details -->
</v-form>
</template>
<script>
import Book from ‘@/models/book‘
export default {
computed: {
book: {
get() {
return {...this.$store.state.selectedBook}
},
set(book) {
this.$store.commit(‘updateSelectedBook‘, book)
}
}
},
methods: {
addBook() {
const book = new Book(this.name, this.author, ...)
this.$store.commit(‘addBook‘, book)
this.$store.commit(‘closeAddBook‘)
}
}
}
</script>
The component syncs with the Vuex selectedBook
state and populates the form using two-way binding.
On submit, it instantiates a new Book and dispatches a mutation to add it to state.
In the store, handle adding books:
export const mutations = {
updateSelectedBook(state, book) {
state.selectedBook = book
},
addBook(state, book) {
state.books.push(book)
}
}
Updating Books
Handle editing books in index.vue
:
editBook(book) {
this.$store.commit(‘openAddBook‘)
this.$store.commit(‘updateSelectedBook‘, book)
}
This dispatches mutations to open the add book dialog and populate the form for editing.
Saving the form commits the updated book object to state like when adding a new book.
Deleting Books
Implement deleting books:
deleteBook(book) {
this.$store.commit(‘removeBook‘, book)
}
removeBook(state, bookToRemove) {
state.books = state.books.filter(book => book !== bookToRemove)
}
This commits a mutation by passing the book object to filter it out of state.
Fetching Data from API
Instead of mock data, we can make API calls to fetch and persist books.
Install the axios
Nuxt module earlier.
Create an API util file utils/api.js
with a getBooks
method:
import axios from ‘axios‘
export default {
getBooks() {
return axios.get(‘/books‘)
}
}
Use this in index.vue
:
export default {
async asyncData() {
const books = await api.getBooks()
return {
books
}
}
}
This fetches the books on the server before rendering the page.
Do the same for other data fetching and persistence.
Conclusion
And that‘s it! We‘ve built a CRUD book store application with Nuxt and Vuetify from scratch.
The key things we learned were:
- Scaffolding a Nuxt app with useful modules
- Structuring the project
- Building reusable components
- Centralizing state with Vuex
- Basic CRUD functionality
- Fetching data on server-side
There‘s a lot more we can do to expand this including improving styling, adding authentication, writing tests and deployment.
For further learning, check out the official Nuxt and Vuetify documentation.
Let me know in the comments about what you would like to see next!