Creating a CRUD (Create, Read, Update, Delete) application using Vue.js and the Composition API is a modern approach to managing state and logic in your Vue applications. This guide will walk you through the entire process, including setting up a JSON server for local storage, implementing form validation, error handling, and designing a basic GUI for all components.
Introduction
Vue.js has consistently evolved, offering powerful tools to create reactive web applications. The Composition API, introduced in Vue 3, provides a flexible and scalable way to manage component logic. In this tutorial, we’ll build a CRUD app using Vue’s Composition API, backed by a JSON server for data storage. We’ll also ensure our application handles errors gracefully and validates user input effectively.
Prerequisites
Before we start, ensure you have the following installed:
Project Setup
Initialize a Vue Project
vue create vue-crud-app
cd vue-crud-app
Explanation of above code
vue create vue-crud-app
Create a new Vue project:
vue create
is a command from the Vue CLI that initializes a new Vue.js project.vue-crud-app
is the name of the new project. This command will create a new directory calledvue-crud-app
and set up a basic Vue.js project structure inside it.
During the creation process, Vue CLI will ask you some questions to customize the setup of your new project. You can choose the default settings or manually select the features you need (like TypeScript support, Vue Router, Vuex, etc.).
cd vue-crud-app
Navigate into the new project directory:
cd
stands for “change directory”.- This command moves you into the newly created
vue-crud-app
directory, where your Vue.js project files are located.
After running these commands, you will be inside the vue-crud-app
directory, and you can start developing your Vue.js application. Typically, the next steps would involve installing additional dependencies, configuring your project, and starting the development server
2. Install Axios and JSON Server
npm install axios json-server
npm install axios json-server
This command installs two packages, axios
and json-server
, as dependencies in your Vue.js project. Here’s what each of these packages does:
axios
:axios
is a promise-based HTTP client for the browser and Node.js. It makes it easier to make HTTP requests (like GET, POST, PUT, DELETE) to interact with APIs.- In the context of a Vue.js project,
axios
is commonly used to fetch data from an API and handle it within your components.
json-server
:json-server
is a full fake REST API that you can use for prototyping or as a mock server. It allows you to create a mock JSON-based REST API very quickly.- This is particularly useful during development when you need a backend to test against but don’t have one available yet.
This will add axios
and json-server
to your project’s node_modules
directory and update your package.json
with these dependencies.
3. Set Up JSON Server
Create a db.json
file at the root of your project:
{
"users": []
}
Add a script in package.json
to run the JSON server:
"scripts": {
"serve": "vue-cli-service serve",
"backend": "json-server --watch db.json --port 3000"
}
Run the JSON server:
npm run backend
Building the CRUD Components
1. Create Components Structure
Create the following components:
UserList.vue
UserForm.vue
2. Design Basic GUI
UserList.vue
<template>
<div>
<h1>User List</h1>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
<button @click="editUser(user)">Edit</button>
<button @click="deleteUser(user.id)">Delete</button>
</li>
</ul>
<button @click="showForm = true">Add User</button>
<UserForm v-if="showForm" @save="fetchUsers" @close="showForm = false" />
</div>
</template>
<script setup>
import axios from 'axios';
import { ref, onMounted } from 'vue';
import UserForm from './UserForm.vue';
const users = ref([]);
const showForm = ref(false);
const fetchUsers = async () => {
try {
const response = await axios.get('http://localhost:3000/users');
users.value = response.data;
} catch (error) {
console.error('Error fetching users:', error);
}
};
const deleteUser = async (id) => {
try {
await axios.delete(`http://localhost:3000/users/${id}`);
fetchUsers();
} catch (error) {
console.error('Error deleting user:', error);
}
};
const editUser = (user) => {
// Logic to handle editing a user
};
onMounted(fetchUsers);
</script>
<style scoped>
/* Add your styles here */
</style>
Explanation of above code
This code is a Vue.js component that displays a list of users, allows you to delete users, and provides functionality to add or edit users using a form. Here’s a breakdown of each part:
<template>
<div>
<h1>User List</h1>
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }} - {{ user.email }}
<button @click="editUser(user)">Edit</button>
<button @click="deleteUser(user.id)">Delete</button>
</li>
</ul>
<button @click="showForm = true">Add User</button>
<UserForm v-if="showForm" @save="fetchUsers" @close="showForm = false" />
</div>
</template>
Explanation
<div>
:- A container for all the content in the component.
<h1>User List</h1>
:- A heading that displays “User List”.
<ul>
:- An unordered list to hold the list items (
<li>
) of users.
- An unordered list to hold the list items (
<li v-for="user in users" :key="user.id">
:v-for
: A directive to loop through each user in theusers
array and create a list item for each user.user in users
: The syntax to iterate over theusers
array, whereuser
represents the current item in the loop.:key="user.id"
: A unique key binding to each list item, which helps Vue keep track of items for efficient updates.
{{ user.name }} - {{ user.email }}
:- Interpolation to display the user’s name and email.
<button @click="editUser(user)">Edit</button>
:- A button to edit the user.
@click="editUser(user)"
: An event listener that calls theeditUser
method with the user as an argument when the button is clicked.
<button @click="deleteUser(user.id)">Delete</button>
:- A button to delete the user.
@click="deleteUser(user.id)"
: An event listener that calls thedeleteUser
method with the user’s ID as an argument when the button is clicked.
<button @click="showForm = true">Add User</button>
:- A button to show the form for adding a new user.
@click="showForm = true"
: An event listener that sets theshowForm
variable totrue
when the button is clicked, making the form visible.
<UserForm v-if="showForm" @save="fetchUsers" @close="showForm = false" />
:UserForm
: A custom component for the user form.v-if="showForm"
: A directive that conditionally renders theUserForm
component ifshowForm
istrue
.@save="fetchUsers"
: An event listener that calls thefetchUsers
method when thesave
event is emitted from theUserForm
component.@close="showForm = false"
: An event listener that setsshowForm
tofalse
when theclose
event is emitted from theUserForm
component.
Script
The script section contains the logic of your Vue component. It is where you define data, methods, and lifecycle hooks.
<script setup>
import axios from 'axios';
import { ref, onMounted } from 'vue';
import UserForm from './UserForm.vue';
const users = ref([]);
const showForm = ref(false);
const fetchUsers = async () => {
try {
const response = await axios.get('http://localhost:3000/users');
users.value = response.data;
} catch (error) {
console.error('Error fetching users:', error);
}
};
const deleteUser = async (id) => {
try {
await axios.delete(`http://localhost:3000/users/${id}`);
fetchUsers();
} catch (error) {
console.error('Error deleting user:', error);
}
};
const editUser = (user) => {
// Logic to handle editing a user
};
onMounted(fetchUsers);
</script>
Explanation
<script setup>
:- A new, simpler syntax in Vue 3 for using the Composition API. It allows you to write less boilerplate code and provides better TypeScript support.
import axios from 'axios';
:- Importing
axios
, a library to make HTTP requests.
- Importing
import { ref, onMounted } from 'vue';
:- Importing
ref
andonMounted
from Vue’s Composition API. ref
: A function to create reactive references for state management.onMounted
: A lifecycle hook that runs code when the component is mounted (i.e., inserted into the DOM).
- Importing
import UserForm from './UserForm.vue';
:- Importing the
UserForm
component.
- Importing the
const users = ref([]);
:- Creating a reactive reference
users
initialized with an empty array to store the list of users.
- Creating a reactive reference
const showForm = ref(false);
:- Creating a reactive reference
showForm
initialized withfalse
to control the visibility of the user form.
- Creating a reactive reference
const fetchUsers = async () => { ... };
:- Defining an asynchronous function
fetchUsers
to fetch user data from the server. - Inside this function:
await axios.get('http://localhost:3000/users')
: Making a GET request to the server to retrieve the list of users.users.value = response.data
: Updating theusers
array with the fetched data.console.error('Error fetching users:', error)
: Logging any errors that occur during the request.
- Defining an asynchronous function
const deleteUser = async (id) => { ... };
:- Defining an asynchronous function
deleteUser
to delete a user by their ID. - Inside this function:
await axios.delete(
http://localhost:3000/users/${id}`)`: Making a DELETE request to the server to delete the user.fetchUsers()
: Refreshing the user list after deletion.console.error('Error deleting user:', error)
: Logging any errors that occur during the request.
- Defining an asynchronous function
const editUser = (user) => { ... };
:- Defining a function
editUser
to handle editing a user. The logic for this function needs to be implemented based on your specific requirements.
- Defining a function
onMounted(fetchUsers);
:- Using the
onMounted
lifecycle hook to call thefetchUsers
function when the component is mounted, ensuring the user list is loaded when the component is first rendered.
- Using the
<style scoped>
/* Add your styles here */
</style>
Style
- The
<style scoped>
section is for adding component-specific styles. - The
scoped
attribute ensures that these styles apply only to this component.
UserForm Component:
- Ensure that you have a
UserForm.vue
component that handles the form for adding and editing users. - The
UserForm
component should emitsave
andclose
events, which the parent component listens to.
- Ensure that you have a
JSON Server Setup:
- Start a JSON server with a command like
json-server --watch db.json
wheredb.json
contains your mock data.
- Start a JSON server with a command like
Summary
- Template: Defines the structure and layout of the component’s HTML, including directives and event listeners.
- Script: Contains the logic of the component, including data state management, methods, and lifecycle hooks.
- Style: Contains the component-specific styles.
UserForm.vue
<template>
<div>
<h2 v-if="user.id">Edit User</h2>
<h2 v-else>Add User</h2>
<form @submit.prevent="saveUser">
<label>
Name:
<input v-model="user.name" required />
</label>
<label>
Email:
<input v-model="user.email" required type="email" />
</label>
<button type="submit">Save</button>
<button @click="closeForm">Cancel</button>
</form>
</div>
</template>
<script setup>
import axios from 'axios';
import { ref, watch } from 'vue';
const props = defineProps(['editUser']);
const emit = defineEmits(['save', 'close']);
const user = ref({ name: '', email: '' });
watch(
() => props.editUser,
(newUser) => {
if (newUser) {
user.value = { ...newUser };
}
},
{ immediate: true }
);
const saveUser = async () => {
try {
if (user.value.id) {
await axios.put(`http://localhost:3000/users/${user.value.id}`, user.value);
} else {
await axios.post('http://localhost:3000/users', user.value);
}
emit('save');
closeForm();
} catch (error) {
console.error('Error saving user:', error);
}
};
const closeForm = () => {
user.value = { name: '', email: '' };
emit('close');
};
</script>
<style scoped>
/* Add your styles here */
</style>
Let’s break down this code, which is a Vue component for a user form that can either add a new user or edit an existing user.
<template>
<div>
<h2 v-if="user.id">Edit User</h2>
<h2 v-else>Add User</h2>
<form @submit.prevent="saveUser">
<label>
Name:
<input v-model="user.name" required />
</label>
<label>
Email:
<input v-model="user.email" required type="email" />
</label>
<button type="submit">Save</button>
<button @click="closeForm">Cancel</button>
</form>
</div>
</template>
Template
The template defines the structure of the form and how it is displayed:
Conditional Headings:
<h2 v-if="user.id">Edit User</h2>
: Displays “Edit User” if theuser
object has anid
property (indicating that this is an existing user being edited).<h2 v-else>Add User</h2>
: Displays “Add User” if theuser
object does not have anid
property (indicating that this is a new user being added).
Form:
<form @submit.prevent="saveUser">
: The@submit.prevent
directive listens for the form’s submit event and prevents the default form submission, instead calling thesaveUser
method.
<label>
Name:
<input v-model="user.name" required />
</label>
Name Input:
<input v-model="user.name" required />
: A text input bound to thename
property of theuser
object. Thev-model
directive creates a two-way binding, automatically updating theuser.name
value as the user types. Therequired
attribute ensures this field must be filled out.
<label>
Email:
<input v-model="user.email" required type="email" />
</label>
Email Input:
<input v-model="user.email" required type="email" />
: A text input bound to theemail
property of theuser
object. Thetype="email"
attribute specifies that this input should be treated as an email address, and therequired
attribute ensures this field must be filled out- Buttons:
<button type="submit">Save</button>
: A submit button to save the user.<button @click="closeForm">Cancel</button>
: A button to cancel and close the form, calling thecloseForm
method when clicked.
<script setup>
import axios from 'axios';
import { ref, watch } from 'vue';
const props = defineProps(['editUser']);
const emit = defineEmits(['save', 'close']);
const user = ref({ name: '', email: '' });
watch(
() => props.editUser,
(newUser) => {
if (newUser) {
user.value = { ...newUser };
}
},
{ immediate: true }
);
const saveUser = async () => {
try {
if (user.value.id) {
await axios.put(`http://localhost:3000/users/${user.value.id}`, user.value);
} else {
await axios.post('http://localhost:3000/users', user.value);
}
emit('save');
closeForm();
} catch (error) {
console.error('Error saving user:', error);
}
};
const closeForm = () => {
user.value = { name: '', email: '' };
emit('close');
};
</script>
Script
The script contains the logic for the form:
Imports:
import axios from 'axios';
: Importing Axios for making HTTP requests.import { ref, watch } from 'vue';
: Importingref
for reactive state andwatch
for watching changes in properties.
Props and Emits:
const props = defineProps(['editUser']);
: Declaring aprops
object to receive aneditUser
prop from the parent component.const emit = defineEmits(['save', 'close']);
: Declaring events that this component can emit (save
andclose
).
Reactive State:
const user = ref({ name: '', email: '' });
: Creating a reactive referenceuser
to store the form data.
Watch Property:
watch(() => props.editUser, (newUser) => { ... }, { immediate: true });
: Watching for changes to theeditUser
prop. If a new user is passed in, theuser
state is updated to reflect the new user’s data. Theimmediate: true
option ensures the watcher runs immediately when the component is created.
Save User Function:
const saveUser = async () => { ... };
: An asynchronous function to save the user.- Inside
saveUser
:- If the
user
has anid
(indicating an existing user), it sends a PUT request to update the user. - If the
user
does not have anid
(indicating a new user), it sends a POST request to create a new user. - After saving, it emits the
save
event and callscloseForm
.
- If the
Close Form Function:
const closeForm = () => { ... };
: A function to reset the form and emit theclose
event.- Inside
closeForm
:- It resets the
user
object to its initial state. - It emits the
close
event.
- It resets the
Error Handling and Validation
To manage errors and validate form inputs, we can enhance the UserForm component.
Enhanced UserForm.vue
<template>
<div>
<h2 v-if="user.id">Edit User</h2>
<h2 v-else>Add User</h2>
<form @submit.prevent="validateAndSave">
<div>
<label>Name:</label>
<input v-model="user.name" />
<span v-if="errors.name">{{ errors.name }}</span>
</div>
<div>
<label>Email:</label>
<input v-model="user.email" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>
<button type="submit">Save</button>
<button @click="closeForm">Cancel</button>
<div v-if="errorMessage">{{ errorMessage }}</div>
</form>
</div>
</template>
<script setup>
import axios from 'axios';
import { ref, watch } from 'vue';
const props = defineProps(['editUser']);
const emit = defineEmits(['save', 'close']);
const user = ref({ name: '', email: '' });
const errors = ref({});
const errorMessage = ref('');
watch(
() => props.editUser,
(newUser) => {
if (newUser) {
user.value = { ...newUser };
}
},
{ immediate: true }
);
const validateAndSave = async () => {
if (!validateForm()) return;
try {
if (user.value.id) {
await axios.put(`http://localhost:3000/users/${user.value.id}`, user.value);
} else {
await axios.post('http://localhost:3000/users', user.value);
}
emit('save');
closeForm();
} catch (error) {
errorMessage.value = 'Error saving user: ' + error.message;
}
};
const validateForm = () => {
errors.value = {};
if (!user.value.name) errors.value.name = 'Name is required';
if (!user.value.email) errors.value.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(user.value.email)) errors.value.email = 'Email is invalid';
return Object.keys(errors.value).length === 0;
};
const closeForm = () => {
user.value = { name: '', email: '' };
errors.value = {};
errorMessage.value = '';
emit('close');
};
</script>
<style scoped>
/* Add your styles here */
</style>
Let’s break down the updated Vue component code with detailed explanations for each part to help beginners understand it thoroughly.
<template>
<div>
<h2 v-if="user.id">Edit User</h2>
<h2 v-else>Add User</h2>
<form @submit.prevent="validateAndSave">
<div>
<label>Name:</label>
<input v-model="user.name" />
<span v-if="errors.name">{{ errors.name }}</span>
</div>
<div>
<label>Email:</label>
<input v-model="user.email" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>
<button type="submit">Save</button>
<button @click="closeForm">Cancel</button>
<div v-if="errorMessage">{{ errorMessage }}</div>
</form>
</div>
</template>
Template
The template defines the structure of the form and how it is displayed. It includes form validation messages and an error message area.
Explanation
Conditional Headings:
<h2 v-if="user.id">Edit User</h2>
: Displays “Edit User” if theuser
object has anid
property, indicating that this is an existing user being edited.<h2 v-else>Add User</h2>
: Displays “Add User” if theuser
object does not have anid
property, indicating that this is a new user being added.
Form:
<form @submit.prevent="validateAndSave">
: The@submit.prevent
directive listens for the form’s submit event, prevents the default form submission, and calls thevalidateAndSave
method.
<div>
<label>Name:</label>
<input v-model="user.name" />
<span v-if="errors.name">{{ errors.name }}</span>
</div>
Name Input:
<input v-model="user.name" />
: A text input bound to thename
property of theuser
object. Thev-model
directive creates a two-way binding, automatically updating theuser.name
value as the user types.<span v-if="errors.name">{{ errors.name }}</span>
: Displays an error message if there is an error related to thename
field.
<div>
<label>Email:</label>
<input v-model="user.email" type="email" />
<span v-if="errors.email">{{ errors.email }}</span>
</div>
Email Input:
<input v-model="user.email" type="email" />
: A text input bound to theemail
property of theuser
object. Thetype="email"
attribute specifies that this input should be treated as an email address.<span v-if="errors.email">{{ errors.email }}</span>
: Displays an error message if there is an error related to theemail
field.
Error Message:
<div v-if="errorMessage">{{ errorMessage }}</div>
: Displays an error message if there is a problem saving the user.
<script setup>
import axios from 'axios';
import { ref, watch } from 'vue';
const props = defineProps(['editUser']);
const emit = defineEmits(['save', 'close']);
const user = ref({ name: '', email: '' });
const errors = ref({});
const errorMessage = ref('');
watch(
() => props.editUser,
(newUser) => {
if (newUser) {
user.value = { ...newUser };
}
},
{ immediate: true }
);
const validateAndSave = async () => {
if (!validateForm()) return;
try {
if (user.value.id) {
await axios.put(`http://localhost:3000/users/${user.value.id}`, user.value);
} else {
await axios.post('http://localhost:3000/users', user.value);
}
emit('save');
closeForm();
} catch (error) {
errorMessage.value = 'Error saving user: ' + error.message;
}
};
const validateForm = () => {
errors.value = {};
if (!user.value.name) errors.value.name = 'Name is required';
if (!user.value.email) errors.value.email = 'Email is required';
else if (!/\S+@\S+\.\S+/.test(user.value.email)) errors.value.email = 'Email is invalid';
return Object.keys(errors.value).length === 0;
};
const closeForm = () => {
user.value = { name: '', email: '' };
errors.value = {};
errorMessage.value = '';
emit('close');
};
</script>
Script
The script contains the logic for the form, including form validation and saving the user data.
Explanation
Imports:
import axios from 'axios';
: Importing Axios for making HTTP requests.import { ref, watch } from 'vue';
: Importingref
for reactive state andwatch
for watching changes in properties.
Props and Emits:
const props = defineProps(['editUser']);
: Declaring aprops
object to receive aneditUser
prop from the parent component.const emit = defineEmits(['save', 'close']);
: Declaring events that this component can emit (save
andclose
).
Reactive State:
const user = ref({ name: '', email: '' });
: Creating a reactive referenceuser
to store the form data.const errors = ref({});
: Creating a reactive referenceerrors
to store validation error messages.const errorMessage = ref('');
: Creating a reactive referenceerrorMessage
to store general error messages.
Watch Property:
watch(() => props.editUser, (newUser) => { ... }, { immediate: true });
: Watching for changes to theeditUser
prop. If a new user is passed in, theuser
state is updated to reflect the new user’s data. Theimmediate: true
option ensures the watcher runs immediately when the component is created.
Validation and Save Function:
const validateAndSave = async () => { ... };
: An asynchronous function that validates the form and then saves the user.- Inside
validateAndSave
:if (!validateForm()) return;
: CallsvalidateForm
to check for errors. If there are errors, it stops the function.await axios.put
orawait axios.post
: Depending on whether theuser
has anid
, it sends a PUT request to update the user or a POST request to create a new user.emit('save');
: Emits thesave
event after the user is saved.closeForm();
: CallscloseForm
to reset the form.
Form Validation Function:
const validateForm = () => { ... };
: A function to validate the form fields.- Inside
validateForm
:errors.value = {};
: Resets the errors.- Checks for empty
name
andemail
fields, and validates the email format using a regular expression. - Returns
true
if there are no errors, otherwise returnsfalse
.
Close Form Function:
const closeForm = () => { ... };
: A function to reset the form and emit theclose
event.- Inside
closeForm
:- Resets the
user
object,errors
, anderrorMessage
. - Emits the
close
event.
- Resets the
In this tutorial, we built a simple CRUD app using Vue’s Composition API and a JSON server for local data storage. We covered component design, error handling, and form validation. This setup provides a strong foundation for building more complex Vue applications.
By following the steps outlined, you can extend the functionality, add more features, and integrate this approach into larger projects. Don’t forget to experiment and customize according to your project’s needs.
Welcome to DevTechTutor.com, your ultimate resource for mastering web development and technology! Whether you're a beginner eager to dive into coding or an experienced developer looking to sharpen your skills, DevTechTutor.com is here to guide you every step of the way. Our mission is to make learning web development accessible, engaging, and effective.