HNT-START
TITLE: How to Retrieve Data From Database Using Vue in Laravel
SLUG: how-to-retrieve-data-from-database-using-vue
EXCERPT: Learn how to fetch MySQL records in Laravel using Vue 3, Eloquent, JSON routes, and the Fetch API, with working code and fixes for common errors.

CATEGORY: Laravel

Your Vue component shows an empty list even though your Laravel database contains records. You can fix this by creating a Laravel JSON endpoint, fetching its response with Vue, and displaying the results using v-for.

If you're searching for how to retrieve data from database using vue, the correct approach is to let Laravel handle the database connection and send the results to Vue as JSON. Vue uses fetch() or Axios to request that data, stores it in a reactive variable, and updates the page automatically.

We'll build a working product listing using Laravel, MySQL, Vue 3, and Vite. The final page will load product names and prices directly from your database, with proper loading and error handling.

How to retrieve data from database using Vue in Laravel: 6 steps

Before writing code, understand how Laravel and Vue communicate.

Vue runs in the browser. It cannot directly execute Laravel's Eloquent queries or access your MySQL database securely.

Laravel runs on the server. It reads database records and returns the information that the frontend needs.

The request follows this flow:

User opens /products | v Laravel returns Blade page | v Vue component loads | v fetch('/api/products') | v Laravel ProductController | v Product::get() | v MySQL products table | v JSON response | v Vue displays products 

This separation is important.

You don't put SQL queries inside Vue components, and you don't need to reload the entire page every time the displayed data changes.

For this example, we'll use an existing Laravel application with Vue 3 installed through npm. If Vue isn't installed yet, the setup is included below.

Step 1: Configure the Laravel MySQL database connection

Open your Laravel project's .env file.

Find the database settings and update them:

DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=vue_demo DB_USERNAME=root DB_PASSWORD= 

These values assume a local MySQL development environment.

Change the username and password according to your MySQL configuration. For production applications, use a dedicated database account with appropriate permissions instead of root.

The database must exist before Laravel can create tables inside it.

Open phpMyAdmin or a MySQL command-line session and execute:

CREATE DATABASE IF NOT EXISTS vue_demo; 

Now clear Laravel's cached configuration:

php artisan config:clear 

If you're using XAMPP, make sure MySQL is running in the XAMPP Control Panel.

You can confirm that MySQL accepts connections by running:

mysql -h 127.0.0.1 -P 3306 -u root -p 

Enter the configured MySQL password when prompted.

If the account has no password in your local environment, use the appropriate login settings.

Once connected, run:

SHOW DATABASES; 

You should see vue_demo in the results.

If Laravel reports:

SQLSTATE[HY000] [2002] Connection refused 

Check whether MySQL is running and whether the port matches your .env configuration.

If you see:

SQLSTATE[HY000] [1045] Access denied for user 

Check your credentials and database permissions.

For a closer explanation of local and remote MySQL connections, read our guide to the host parameter in MySQL connections.

What to expect: Laravel now has the connection settings needed to access the vue_demo database.

Step 2: Create the Product model and products table

We'll use a small products table so it's easy to see exactly where the data comes from.

Run this command in your Laravel project directory:

php artisan make:model Product -m 

Laravel creates a model:

app/Models/Product.php 

And a migration:

database/migrations/xxxx_xx_xx_xxxxxx_create_products_table.php 

The timestamp in the migration filename will be different in your project.

Open the migration file and replace its contents with:

<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('products', function (Blueprint $table) { $table->id(); $table->string('name'); $table->decimal('price', 10, 2); $table->timestamps(); }); } public function down(): void { Schema::dropIfExists('products'); } }; 

This creates a table with the following columns:

id name price created_at updated_at 

The id column is the primary key.

The price column uses decimal(10,2), which is appropriate for storing fixed-precision prices in this example.

Next, open:

app/Models/Product.php 

Update it:

<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; class Product extends Model { protected $fillable = [ 'name', 'price', ]; } 

The $fillable property allows these fields to be assigned when using Eloquent's create() method.

Now execute:

php artisan migrate 

Laravel should report that the products migration ran successfully.

If you already have a products table, don't run a duplicate table-creation migration. Use your existing table structure or create a migration that changes only the columns you need.

Insert sample records

Open Laravel Tinker:

php artisan tinker 

Insert some products:

\App\Models\Product::create([ 'name' => 'Laptop', 'price' => 2500.00, ]); \App\Models\Product::create([ 'name' => 'Keyboard', 'price' => 150.00, ]); \App\Models\Product::create([ 'name' => 'Mouse', 'price' => 75.00, ]); \App\Models\Product::create([ 'name' => 'Monitor', 'price' => 850.00, ]); 

You should see Eloquent model output after each successful insertion.

Exit Tinker:

exit 

Check the records in MySQL:

SELECT id, name, price FROM products ORDER BY id; 

For a new, empty table, the result should look like this:

+----+----------+---------+ | id | name | price | +----+----------+---------+ | 1 | Laptop | 2500.00 | | 2 | Keyboard | 150.00 | | 3 | Mouse | 75.00 | | 4 | Monitor | 850.00 | +----+----------+---------+ 

If your table already contained data, the IDs may be different.

What to expect: Four product records are now stored in MySQL and can be retrieved through Laravel.

Step 3: Create a Laravel controller to fetch MySQL records

Vue needs an HTTP endpoint that returns product data.

We'll create that endpoint using a Laravel controller.

Run:

php artisan make:controller ProductController 

Open:

app/Http/Controllers/ProductController.php 

Replace the contents with:

<?php namespace App\Http\Controllers; use App\Models\Product; use Illuminate\Http\JsonResponse; class ProductController extends Controller { public function index(): JsonResponse { $products = Product::query() ->select('id', 'name', 'price') ->orderBy('id', 'asc') ->get(); return response()->json($products); } } 

This is where Laravel retrieves data from MySQL.

Let's examine the query.

Product::query() 

Creates an Eloquent query for the products table.

->select('id', 'name', 'price') 

Selects only the fields needed by the frontend.

->orderBy('id', 'asc') 

Sorts the records by ID.

->get() 

Executes the query and retrieves the matching products.

Finally:

return response()->json($products); 

Converts the result into a JSON HTTP response.

For this type of application, returning JSON is better than sending generated HTML from the controller. Vue can decide how to display the records without changing the backend response.

There's also no need to return every column from the database.

For example, if your products table later contains internal costs or supplier information, those fields shouldn't automatically become visible to every frontend user.

When working with more complex conditions, our tutorial on selecting SQL columns based on conditions covers the database-query side.

What to expect: Your controller can retrieve the product records and return only the selected fields.

Step 4: Create a Laravel route that Vue can call

Now we need a URL for the controller.

Open:

routes/web.php 

Add:

use App\Http\Controllers\ProductController; use Illuminate\Support\Facades\Route; Route::get('/api/products', [ProductController::class, 'index']); Route::view('/products', 'products'); 

The first route returns JSON:

GET /api/products 

The second route displays the page that will contain our Vue component:

GET /products 

Why put these routes inside web.php?

For this tutorial, Vue and Laravel use the same origin. A JSON GET route inside web.php is enough, and it works without installing Laravel's optional API routing setup.

If you're building a separate REST API, you can use routes/api.php instead.

In Laravel installations where API routing hasn't been configured, you can enable it using:

php artisan install:api 

That command may also install API authentication-related dependencies and generate additional files. It isn't required for this example.

If you define a route in routes/api.php, remember that Laravel normally applies the /api prefix automatically.

For example:

Route::get('/products', [ProductController::class, 'index']); 

Would become:

/api/products 

Don't accidentally define /api/products inside api.php and end up requesting /api/api/products.

Test the JSON endpoint before writing Vue

Start Laravel:

php artisan serve 

Open this URL in your browser:

http://127.0.0.1:8000/api/products 

The response should resemble:

[ { "id": 1, "name": "Laptop", "price": "2500.00" }, { "id": 2, "name": "Keyboard", "price": "150.00" }, { "id": 3, "name": "Mouse", "price": "75.00" }, { "id": 4, "name": "Monitor", "price": "850.00" } ] 

Notice that price may be returned as a string.

This is normal when MySQL decimal values are serialized through Laravel.

If the endpoint doesn't return records, stop here and investigate the Laravel response.

Don't start changing Vue code to fix a broken controller or database connection.

Step 5: Install Vue 3 and connect it to Laravel Vite

Laravel uses Vite to build frontend assets.

We'll install Vue 3 and its Vite plugin.

From the Laravel project directory, execute:

npm install 

Then:

npm install vue 

Install the Vue plugin:

npm install --save-dev @vitejs/plugin-vue 

Now open:

vite.config.js 

For a standard Laravel project with JavaScript and CSS entry points, use:

import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [ laravel({ input: [ 'resources/css/app.css', 'resources/js/app.js', ], refresh: true, }), vue(), ], }); 

If the project already has custom Vite plugins or asset paths, preserve them and add Vue support to the existing configuration.

Next, open:

resources/js/app.js 

Replace or adjust it with:

import './bootstrap'; import { createApp } from 'vue'; import ProductsList from './components/ProductsList.vue'; createApp(ProductsList).mount('#app'); 

The createApp() function initializes Vue.

The mount('#app') instruction tells Vue where to render the component.

Later, our Blade page will contain:

<div id="app"></div> 

Vue will mount inside that element.

This is a common place where developers get confused.

Laravel Blade and Vue can work together, but they do different jobs.

Blade generates the initial HTML page on the server. Vue runs in the browser after the JavaScript loads.

You don't need to transfer every database record into Blade variables when Vue is already fetching a JSON endpoint.

For projects that mix server-rendered values and JavaScript components, see how to set variables in Laravel Blade templates.

What to expect: Laravel Vite is now configured to compile and load Vue single-file components.

Step 6: Retrieve and display Laravel database records in Vue

Create this folder if it doesn't exist:

resources/js/components 

Inside that folder, create:

ProductsList.vue 

Add the following code:

<script setup> import { ref, onMounted } from 'vue'; const products = ref([]); const loading = ref(false); const error = ref(''); async function loadProducts() { loading.value = true; error.value = ''; try { const response = await fetch('/api/products', { method: 'GET', headers: { Accept: 'application/json', }, }); if (!response.ok) { throw new Error( `Failed to load products: HTTP ${response.status}` ); } const data = await response.json(); if (!Array.isArray(data)) { throw new Error('Unexpected API response format'); } products.value = data; } catch (err) { error.value = err.message; } finally { loading.value = false; } } onMounted(() => { loadProducts(); }); </script> <template> <main class="products-page"> <h1>Products</h1> <button type="button" @click="loadProducts" :disabled="loading" > {{ loading ? 'Loading...' : 'Refresh products' }} </button> <p v-if="loading"> Loading products... </p> <p v-else-if="error"> {{ error }} </p> <p v-else-if="products.length === 0"> No products found. </p> <table v-else> <thead> <tr> <th>ID</th> <th>Product name</th> <th>Price</th> </tr> </thead> <tbody> <tr v-for="product in products" :key="product.id" > <td>{{ product.id }}</td> <td>{{ product.name }}</td> <td> {{ Number(product.price).toFixed(2) }} </td> </tr> </tbody> </table> </main> </template> <style scoped> .products-page { max-width: 900px; margin: 40px auto; font-family: Arial, sans-serif; } table { width: 100%; border-collapse: collapse; margin-top: 20px; } th, td { border: 1px solid #ddd; padding: 12px; text-align: left; } th { background: #f5f5f5; } button { padding: 10px 16px; cursor: pointer; } </style> 

This component does more than simply fetch records.

It handles the initial request, shows a loading state, detects unsuccessful HTTP responses, checks the returned data format, and displays an error if the request fails.

It also includes a refresh button so you can reload products without refreshing the entire Laravel page.

Why ref() matters

Look at:

const products = ref([]); 

Vue's ref() creates reactive state.

When this line executes:

products.value = data; 

Vue detects the change and updates the table.

You don't need to manually rebuild the HTML or call a DOM update function.

Why onMounted() is used

This code:

onMounted(() => { loadProducts(); }); 

Runs after the component mounts.

It's a natural place to load the initial data for a client-rendered product listing.

Avoid putting the same fetch request in multiple lifecycle hooks unless you specifically need multiple requests.

Otherwise, you may accidentally load the data twice.

Why response.ok should be checked

The Fetch API doesn't automatically reject a promise when a server returns an HTTP error such as 404 or 500.

That's why we check:

if (!response.ok) { throw new Error( `Failed to load products: HTTP ${response.status}` ); } 

Without this check, your code could attempt to display an error response as if it contained products.

Why v-for needs a unique key

The table contains:

<tr v-for="product in products" :key="product.id" > 

The v-for directive repeats the row for each product.

The key helps Vue identify which record belongs to each rendered row when the list changes.

Use a stable database ID rather than the array index when an ID is available.

Create the Laravel Blade page

Now create:

resources/views/products.blade.php 

Add:

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1" > <title>Laravel Vue Products</title> @vite([ 'resources/css/app.css', 'resources/js/app.js' ]) </head> <body> <div id="app"></div> </body> </html> 

The Blade file doesn't need to query the database.

It simply provides the HTML document, Vite assets, and mounting element.

Vue takes care of requesting and displaying the products.

Start the frontend development server:

npm run dev 

Keep the Laravel development server running in another terminal:

php artisan serve 

Now visit:

http://127.0.0.1:8000/products 

You should see a table containing the product records from MySQL.

Verify that Vue is fetching real MySQL data

Open the browser's developer tools and switch to the Network tab.

Refresh the products page.

Look for:

GET /api/products 

Click the request and inspect its status.

A successful response should show:

Status: 200 OK Content-Type: application/json 

The Response or Preview tab should contain the product records.

Your page should display something similar to:

Products [Refresh products] ID Product name Price 1 Laptop 2500.00 2 Keyboard 150.00 3 Mouse 75.00 4 Monitor 850.00 

To confirm that Vue is displaying live database content rather than hardcoded values, open Tinker:

php artisan tinker 

Insert another record:

\App\Models\Product::create([ 'name' => 'USB Cable', 'price' => 25.00, ]); 

Exit Tinker and click the Refresh products button.

The new product should appear.

No changes to the Vue source code are needed.

That confirms the complete connection from MySQL through Laravel to the browser.

Fix Vue showing an empty list when Laravel has records

If the table still doesn't show anything, diagnose the problem from the backend toward the frontend.

That order is faster than changing unrelated Vue settings.

Laravel API returns 404 Not Found

First, check your route:

php artisan route:list --path=api/products 

You should see a GET route pointing to ProductController.

If it isn't present, check routes/web.php.

You can also clear route caching:

php artisan route:clear 

Make sure the frontend requests the correct path:

fetch('/api/products') 

A small mismatch between /api/product and /api/products is enough to return 404.

Vue returns Unexpected token '<', "<!DOCTYPE" is not valid JSON

This error means the response is HTML, not the JSON that response.json() expected.

Common reasons include an incorrect route, a Laravel error page, or a redirect to a login screen.

Open the browser Network tab and inspect the response body.

If it contains an HTML document, find out why the server returned that document.

Don't try to fix the issue by replacing every response.json() call with response.text().

That changes how the response is parsed but doesn't solve the underlying API problem.

API returns an empty array

Suppose /api/products returns:

[] 

That means Laravel successfully returned an empty collection.

Check the records directly:

php artisan tinker 

Run:

\App\Models\Product::count(); 

Then:

\App\Models\Product::all(); 

If the count is zero, your configured database doesn't contain the expected records.

Check DB_DATABASE, the active environment, a...