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 createis a command from the Vue CLI that initializes a new Vue.js project.vue-crud-appis the name of the new project. This command will create a new directory calledvue-crud-appand 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:
cdstands for “change directory”.- This command moves you into the newly created
vue-crud-appdirectory, 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:axiosis 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,
axiosis commonly used to fetch data from an API and handle it within your components.
json-server:json-serveris 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.vueUserForm.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 theusersarray and create a list item for each user.user in users: The syntax to iterate over theusersarray, whereuserrepresents 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 theeditUsermethod 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 thedeleteUsermethod 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 theshowFormvariable totruewhen 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 theUserFormcomponent ifshowFormistrue.@save="fetchUsers": An event listener that calls thefetchUsersmethod when thesaveevent is emitted from theUserFormcomponent.@close="showForm = false": An event listener that setsshowFormtofalsewhen thecloseevent is emitted from theUserFormcomponent.
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
refandonMountedfrom 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
UserFormcomponent.
- Importing the
const users = ref([]);:- Creating a reactive reference
usersinitialized with an empty array to store the list of users.
- Creating a reactive reference
const showForm = ref(false);:- Creating a reactive reference
showForminitialized withfalseto control the visibility of the user form.
- Creating a reactive reference
const fetchUsers = async () => { ... };:- Defining an asynchronous function
fetchUsersto 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 theusersarray 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
deleteUserto 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
editUserto 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
onMountedlifecycle hook to call thefetchUsersfunction 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
scopedattribute ensures that these styles apply only to this component.
UserForm Component:
- Ensure that you have a
UserForm.vuecomponent that handles the form for adding and editing users. - The
UserFormcomponent should emitsaveandcloseevents, 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.jsonwheredb.jsoncontains 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 theuserobject has anidproperty (indicating that this is an existing user being edited).<h2 v-else>Add User</h2>: Displays “Add User” if theuserobject does not have anidproperty (indicating that this is a new user being added).
Form:
<form @submit.prevent="saveUser">: The@submit.preventdirective listens for the form’s submit event and prevents the default form submission, instead calling thesaveUsermethod.
<label>
Name:
<input v-model="user.name" required />
</label>
Name Input:
<input v-model="user.name" required />: A text input bound to thenameproperty of theuserobject. Thev-modeldirective creates a two-way binding, automatically updating theuser.namevalue as the user types. Therequiredattribute 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 theemailproperty of theuserobject. Thetype="email"attribute specifies that this input should be treated as an email address, and therequiredattribute 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 thecloseFormmethod 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';: Importingreffor reactive state andwatchfor watching changes in properties.
Props and Emits:
const props = defineProps(['editUser']);: Declaring apropsobject to receive aneditUserprop from the parent component.const emit = defineEmits(['save', 'close']);: Declaring events that this component can emit (saveandclose).
Reactive State:
const user = ref({ name: '', email: '' });: Creating a reactive referenceuserto store the form data.
Watch Property:
watch(() => props.editUser, (newUser) => { ... }, { immediate: true });: Watching for changes to theeditUserprop. If a new user is passed in, theuserstate is updated to reflect the new user’s data. Theimmediate: trueoption 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
userhas anid(indicating an existing user), it sends a PUT request to update the user. - If the
userdoes not have anid(indicating a new user), it sends a POST request to create a new user. - After saving, it emits the
saveevent and callscloseForm.
- If the
Close Form Function:
const closeForm = () => { ... };: A function to reset the form and emit thecloseevent.- Inside
closeForm:- It resets the
userobject to its initial state. - It emits the
closeevent.
- 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 theuserobject has anidproperty, indicating that this is an existing user being edited.<h2 v-else>Add User</h2>: Displays “Add User” if theuserobject does not have anidproperty, indicating that this is a new user being added.
Form:
<form @submit.prevent="validateAndSave">: The@submit.preventdirective listens for the form’s submit event, prevents the default form submission, and calls thevalidateAndSavemethod.
<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 thenameproperty of theuserobject. Thev-modeldirective creates a two-way binding, automatically updating theuser.namevalue as the user types.<span v-if="errors.name">{{ errors.name }}</span>: Displays an error message if there is an error related to thenamefield.
<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 theemailproperty of theuserobject. 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 theemailfield.
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';: Importingreffor reactive state andwatchfor watching changes in properties.
Props and Emits:
const props = defineProps(['editUser']);: Declaring apropsobject to receive aneditUserprop from the parent component.const emit = defineEmits(['save', 'close']);: Declaring events that this component can emit (saveandclose).
Reactive State:
const user = ref({ name: '', email: '' });: Creating a reactive referenceuserto store the form data.const errors = ref({});: Creating a reactive referenceerrorsto store validation error messages.const errorMessage = ref('');: Creating a reactive referenceerrorMessageto store general error messages.
Watch Property:
watch(() => props.editUser, (newUser) => { ... }, { immediate: true });: Watching for changes to theeditUserprop. If a new user is passed in, theuserstate is updated to reflect the new user’s data. Theimmediate: trueoption 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;: CallsvalidateFormto check for errors. If there are errors, it stops the function.await axios.putorawait axios.post: Depending on whether theuserhas anid, it sends a PUT request to update the user or a POST request to create a new user.emit('save');: Emits thesaveevent after the user is saved.closeForm();: CallscloseFormto 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
nameandemailfields, and validates the email format using a regular expression. - Returns
trueif there are no errors, otherwise returnsfalse.
Close Form Function:
const closeForm = () => { ... };: A function to reset the form and emit thecloseevent.- Inside
closeForm:- Resets the
userobject,errors, anderrorMessage. - Emits the
closeevent.
- 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.