Table of Contents
- Prerequisites
- Setting Up Your Vue.js Project
- Core Vue.js Concepts: Components, Props, and Events
- State Management with Pinia
- Routing with Vue Router
- API Integration and Data Fetching
- Form Handling and Validation
- Testing Vue.js Applications
- Optimizing Performance
- Deploying Your Vue.js App
- Conclusion
- References
Prerequisites
Before diving in, ensure you have the following tools and knowledge:
- Basic JavaScript/HTML/CSS: Familiarity with ES6+ features (arrow functions, async/await, modules) is essential.
- Node.js & npm/yarn: Install Node.js (v14+ recommended) to use
npmoryarnfor package management. - Vue.js Basics: A foundational understanding of Vue (e.g., directives like
v-model, reactivity) will help, but we’ll recap key concepts.
Setting Up Your Vue.js Project
Vue.js offers two primary tools for project scaffolding: Vue CLI (legacy) and Vite (modern, faster). We’ll use Vite for this tutorial due to its superior development experience (instant HMR, optimized builds).
Step 1: Create a Vite Project
Open your terminal and run:
npm create vite@latest my-vue-app -- --template vue
# Or with yarn:
yarn create vite my-vue-app --template vue
Follow the prompts to name your project (e.g., my-vue-app) and select the “Vue” template.
Step 2: Install Dependencies
Navigate to your project folder and install dependencies:
cd my-vue-app
npm install
# Or: yarn install
Step 3: Run the Development Server
Start the dev server to preview your app:
npm run dev
# Or: yarn dev
Visit http://localhost:5173 in your browser—you’ll see a default Vue welcome page.
Project Structure Overview
Vite generates a minimal, organized structure. Key files/folders:
my-vue-app/
├── node_modules/ # Dependencies
├── public/ # Static assets (e.g., images, favicon)
├── src/ # Source code
│ ├── assets/ # Compiled assets (CSS, images)
│ ├── components/ # Reusable Vue components
│ ├── App.vue # Root component
│ └── main.js # Entry point (mounts Vue app)
├── .gitignore # Git ignore rules
├── index.html # HTML entry (Vite injects build output here)
├── package.json # Project metadata and scripts
└── vite.config.js # Vite configuration
Core Vue.js Concepts: Components, Props, and Events
Vue is built around components—reusable, self-contained units of UI. Let’s break down how to create and communicate between components.
Single-File Components (SFCs)
Vue components are typically written as SFCs (.vue files), which combine three sections:
<template>: HTML markup with Vue directives.<script>: JavaScript logic (reactive state, methods, etc.).<style>: CSS styles (scoped to the component by default withscoped).
Example: ProductCard Component
Let’s create a reusable ProductCard component to display product data.
- Create
src/components/ProductCard.vue:
<template>
<div class="product-card">
<img :src="product.image" :alt="product.name" class="product-image" />
<h3 class="product-name">{{ product.name }}</h3>
<p class="product-price">${{ product.price.toFixed(2) }}</p>
<button @click="handleAddToCart" class="add-to-cart-btn">
Add to Cart
</button>
</div>
</template>
<script>
export default {
name: 'ProductCard',
// Define props (data passed from parent)
props: {
product: {
type: Object,
required: true,
// Validate prop structure
validator: (value) => {
return 'name' in value && 'price' in value && 'image' in value;
}
}
},
methods: {
handleAddToCart() {
// Emit event to parent with product data
this.$emit('add-to-cart', this.product);
}
}
};
</script>
<style scoped>
.product-card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
max-width: 250px;
}
.product-image {
width: 100%;
height: 200px;
object-fit: cover;
}
.add-to-cart-btn {
background: #42b983;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
}
</style>
Using ProductCard in a Parent Component
Now, use ProductCard in App.vue to display a list of products:
<template>
<div class="app">
<h1>Our Products</h1>
<div class="product-grid">
<!-- Pass product data as a prop -->
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
</div>
<div class="cart">
<h2>Cart ({{ cart.length }})</h2>
<ul>
<li v-for="item in cart" :key="item.id">{{ item.name }} - ${{ item.price }}</li>
</ul>
</div>
</div>
</template>
<script>
import ProductCard from './components/ProductCard.vue';
export default {
name: 'App',
components: { ProductCard }, // Register the component
data() {
return {
products: [
{
id: 1,
name: 'Vue.js Mug',
price: 19.99,
image: 'https://via.placeholder.com/150?text=Vue+Mug'
},
{
id: 2,
name: 'JavaScript Hoodie',
price: 49.99,
image: 'https://via.placeholder.com/150?text=JS+Hoodie'
}
],
cart: []
};
},
methods: {
addToCart(product) {
this.cart.push(product); // Update cart when event is emitted
}
}
};
</script>
<style>
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 20px;
padding: 20px;
}
.cart {
padding: 20px;
border-top: 1px solid #e0e0e0;
}
</style>
Key Takeaways:
- Props: Pass data from parent to child with
:prop-name. - Events: Emit events from child to parent with
this.$emit('event-name', data), and listen with@event-name. - Reactivity: Vue automatically updates the DOM when
data()properties (likecartorproducts) change.
State Management with Pinia
For larger apps, managing state across components with props/events becomes unwieldy. Pinia (Vue’s official state management library) centralizes state and logic.
Why Pinia?
- Replaces Vuex (Vue’s legacy store) with a simpler API.
- Built-in TypeScript support.
- DevTools integration for time-travel debugging.
Step 1: Install Pinia
npm install pinia
# Or: yarn add pinia
Step 2: Initialize Pinia
Update src/main.js to use Pinia:
import { createApp } from 'vue';
import { createPinia } from 'pinia'; // Import Pinia
import App from './App.vue';
const app = createApp(App);
app.use(createPinia()); // Use Pinia
app.mount('#app');
Step 3: Create a Store
Stores in Pinia are defined with defineStore. Let’s create a cartStore to manage cart state:
Create src/stores/cartStore.js:
import { defineStore } from 'pinia';
// Define store with unique ID 'cart'
export const useCartStore = defineStore('cart', {
// State: Reactive data
state: () => ({
items: []
}),
// Getters: Computed properties (cached)
getters: {
cartCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price, 0)
},
// Actions: Methods to modify state (async allowed)
actions: {
addItem(product) {
this.items.push(product); // 'this' refers to state
},
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
}
}
});
Using the Store in Components
Update App.vue to use cartStore instead of local cart state:
<template>
<!-- ... (keep product grid and ProductCard) -->
<div class="cart">
<h2>Cart ({{ cartStore.cartCount }})</h2>
<p>Total: ${{ cartStore.totalPrice.toFixed(2) }}</p>
<ul>
<li v-for="item in cartStore.items" :key="item.id">
{{ item.name }} - ${{ item.price }}
<button @click="cartStore.removeItem(item.id)">Remove</button>
</li>
</ul>
</div>
</template>
<script>
import ProductCard from './components/ProductCard.vue';
import { useCartStore } from './stores/cartStore'; // Import store
export default {
name: 'App',
components: { ProductCard },
data() {
return {
products: [/* ... */] // Keep product data
};
},
setup() {
const cartStore = useCartStore(); // Initialize store
return { cartStore }; // Expose to template
},
methods: {
addToCart(product) {
this.cartStore.addItem(product); // Call store action
}
}
};
</script>
Routing with Vue Router
Most apps require multiple pages (e.g., product list, product detail). Vue Router handles navigation between views.
Step 1: Install Vue Router
npm install vue-router@4 # Vue Router 4 is compatible with Vue 3
# Or: yarn add vue-router@4
Step 2: Configure Routes
Create src/router/index.js:
import { createRouter, createWebHistory } from 'vue-router';
import ProductList from '../views/ProductList.vue'; // Create this next
import ProductDetail from '../views/ProductDetail.vue'; // Create this next
import Cart from '../views/Cart.vue'; // Create this next
const routes = [
{ path: '/', name: 'ProductList', component: ProductList },
{ path: '/product/:id', name: 'ProductDetail', component: ProductDetail }, // Dynamic route
{ path: '/cart', name: 'Cart', component: Cart }
];
const router = createRouter({
history: createWebHistory(), // Uses HTML5 history mode (no hash in URL)
routes
});
export default router;
Step 3: Create View Components
Views are full-page components (stored in src/views/).
src/views/ProductList.vue (moved product grid from App.vue):
<template>
<div class="product-list">
<h1>Our Products</h1>
<div class="product-grid">
<ProductCard
v-for="product in products"
:key="product.id"
:product="product"
@add-to-cart="addToCart"
/>
</div>
</div>
</template>
<script>
import ProductCard from '../components/ProductCard.vue';
import { useCartStore } from '../stores/cartStore';
export default {
name: 'ProductList',
components: { ProductCard },
data() {
return {
products: [/* ... */] // Product data
};
},
setup() {
const cartStore = useCartStore();
return { cartStore };
},
methods: {
addToCart(product) {
this.cartStore.addItem(product);
}
}
};
</script>
src/views/ProductDetail.vue (dynamic product page):
<template>
<div class="product-detail" v-if="product">
<img :src="product.image" :alt="product.name" class="detail-image" />
<h1>{{ product.name }}</h1>
<p>Price: ${{ product.price }}</p>
<button @click="addToCart" class="add-to-cart-btn">Add to Cart</button>
<router-link to="/">Back to List</router-link>
</div>
<div v-else>Loading...</div>
</template>
<script>
import { useRoute } from 'vue-router'; // Access route params
import { useCartStore } from '../stores/cartStore';
export default {
name: 'ProductDetail',
data() {
return {
product: null,
products: [/* ... */] // Reuse product data (or fetch from API later)
};
},
setup() {
const route = useRoute(); // Get current route
const cartStore = useCartStore();
return { route, cartStore };
},
mounted() {
// Fetch product by ID from route params
this.product = this.products.find(p => p.id === Number(this.route.params.id));
},
methods: {
addToCart() {
this.cartStore.addItem(this.product);
}
}
};
</script>
Step 4: Update App.vue for Routing
Replace App.vue with a layout that includes navigation and a <router-view> (where routed components render):
<template>
<div class="app">
<nav>
<router-link to="/">Products</router-link> |
<router-link to="/cart">Cart ({{ cartStore.cartCount }})</router-link>
</nav>
<router-view /> <!-- Renders current route component -->
</div>
</template>
<script>
import { useCartStore } from './stores/cartStore';
export default {
setup() {
const cartStore = useCartStore();
return { cartStore };
}
};
</script>
<style>
nav {
padding: 20px;
background: #f5f5f5;
}
router-link {
margin-right: 15px;
text-decoration: none;
color: #42b983;
}
router-link.router-link-exact-active {
font-weight: bold;
}
</style>
Step 5: Use the Router in main.js
Update src/main.js to use the router:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import router from './router'; // Import router
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.use(router); // Use router
app.mount('#app');
API Integration and Data Fetching
Real apps fetch data from APIs. We’ll use Axios (a popular HTTP client) to fetch products from a mock API (e.g., JSONPlaceholder or your backend).
Step 1: Install Axios
npm install axios
# Or: yarn add axios
Step 2: Create an API Service
Centralize API calls in a service folder. Create src/services/productService.js:
import axios from 'axios';
// Base URL (use your backend or mock API like JSONPlaceholder)
const API_URL = 'https://jsonplaceholder.typicode.com'; // Mock API (adjust endpoints)
// Fetch all products
export const getProducts = async () => {
const response = await axios.get(`${API_URL}/posts`); // Mock: posts as products
// Transform mock data to match our product structure
return response.data.map(post => ({
id: post.id,
name: `Product ${post.id}`,
price: Math.floor(Math.random() * 100),
image: `https://via.placeholder.com/150?text=Product+${post.id}`,
description: post.body
}));
};
// Fetch product by ID
export const getProductById = async (id) => {
const response = await axios.get(`${API_URL}/posts/${id}`);
return {
id: response.data.id,
name: `Product ${response.data.id}`,
price: Math.floor(Math.random() * 100),
image: `https://via.placeholder.com/150?text=Product+${response.data.id}`,
description: response.data.body
};
};
Step 3: Fetch Data in the Store
Update cartStore to fetch products via the service (instead of hardcoding):
import { defineStore } from 'pinia';
import { getProducts } from '../services/productService';
export const useProductStore = defineStore('product', {
state: () => ({
products: [],
loading: false,
error: null
}),
actions: {
async fetchProducts() {
this.loading = true;
try {
this.products = await getProducts(); // Call API service
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
}
}
});
Step 4: Use Fetched Data in ProductList
Update ProductList.vue to load products from the store:
<script>
import { useProductStore } from '../stores/productStore';
export default {
// ...
async mounted() {
const productStore = useProductStore();
await productStore.fetchProducts(); // Fetch on mount
}
};
</script>
Form Handling and Validation
Forms are critical for user input (e.g., checkout, sign-up). We’ll use VeeValidate (a Vue form validation library) for robust validation.
Step 1: Install VeeValidate
npm install vee-validate @vee-validate/rules
# Or: yarn add vee-validate @vee-validate/rules
Step 2: Create a Checkout Form
Create src/views/Checkout.vue with a validated form:
<template>
<div class="checkout">
<h1>Checkout</h1>
<form @submit.prevent="handleSubmit">
<!-- Name -->
<div class="form-group">
<label>Name</label>
<input v-model="name" type="text" />
<p class="error" v-if="errors.name">{{ errors.name }}</p>
</div>
<!-- Email -->
<div class="form-group">
<label>Email</label>
<input v-model="email" type="email" />
<p class="error" v-if="errors.email">{{ errors.email }}</p>
</div>
<button type="submit">Place Order</button>
</form>
</div>
</template>
<script>
import { useForm } from 'vee-validate';
import { required, email } from '@vee-validate/rules';
export default {
name: 'Checkout',
setup() {
// Define validation rules
const { handleSubmit, errors, values } = useForm({
validationSchema: {
name: required,
email: [required, email]
}
});
// Submit handler
const onSubmit = (values) => {
alert('Order placed!', values);
};
return {
handleSubmit: handleSubmit(onSubmit),
errors,
...values // Expose form values (name, email)
};
}
};
</script>
<style>
.error { color: #ff4444; }
.form-group { margin-bottom: 15px; }
</style>
Testing Vue.js Applications
Testing ensures your app works as expected. We’ll cover unit testing (components) and E2E testing (user flows).
Unit Testing with Jest and Vue Test Utils
Step 1: Install Dependencies
npm install --save-dev jest @vitejs/plugin-vue vue-jest@next @vue/test-utils
Step 2: Test ProductCard Component
Create src/components/__tests__/ProductCard.spec.js:
import { mount } from '@vue/test-utils';
import ProductCard from '../ProductCard.vue';
describe('ProductCard', () => {
const mockProduct = {
id: 1,
name: 'Test Product',
price: 29.99,
image: 'test.jpg'
};
it('renders product name and price', () => {
const wrapper = mount(ProductCard, { props: { product: mockProduct } });
expect(wrapper.find('.product-name').text()).toBe('Test Product');
expect(wrapper.find('.product-price').text()).toBe('$29.99');
});
it('emits "add-to-cart" event when button is clicked', async () => {
const wrapper = mount(ProductCard, { props: { product: mockProduct } });
await wrapper.find('.add-to-cart-btn').trigger('click');
expect(wrapper.emitted('add-to-cart')).toHaveLength(1);
expect(wrapper.emitted('add-to-cart')[0][0]).toEqual(mockProduct);
});
});
E2E Testing with Cypress
Step 1: Install Cypress
npm install --save-dev cypress
Step 2: Add Cypress Script
Update package.json:
"scripts": {
"cypress:open": "cypress open"
}
Step 3: Write an E2E Test
Create cypress/e2e/product-listing.cy.js:
describe('Product Listing', () => {
it('loads products and adds to cart', () => {
cy.visit('/');
cy.contains('Our Products');
cy.get('.product-card').should('have.length.greaterThan', 0);
cy.get('.add-to-cart-btn').first().click();
cy.get('.cart-count').should('contain', '1');
});
});
Optimizing Performance
Optimize your app for speed with these techniques:
-
Code Splitting: Lazy-load routes to reduce initial bundle size:
// In router/index.js { path: '/product/:id', component: () => import('../views/ProductDetail.vue') // Lazy load } -
Image Optimization: Use
loading="lazy"for images and compress assets. -
Avoid Unnecessary Re-renders: Use
v-memoto cache components orshallowReffor large objects.
Deploying Your Vue.js App
Deploy your app to a hosting service like Netlify or Vercel:
Step 1: Build the App
Generate a production build:
npm run build
# Or: yarn build
This creates a dist/ folder with optimized assets.
Step 2: Deploy to Netlify
- Push your code to GitHub.
- Go to Netlify → “New site from Git” → Connect your repo.
- Set build command:
npm run build, publish directory:dist. - Click “Deploy”—your app will be live!
Conclusion
You’ve built a real-world Vue.js application with components, routing, state management, API integration, forms, testing, and deployment. Vue’s flexibility and ecosystem make it ideal for projects of all sizes. To deepen your skills:
- Explore Vue 3’s Composition API for better code organization.
- Add TypeScript for type safety.
- Integrate a backend (e.g., Firebase, Node.js) for persistent data.