How to Retrieve Data From Database Using Vue in Laravel
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, and any query conditions.
A frequent mistake is inserting records into one database through phpMyAdmin while Laravel is connected to another.
Laravel returns a 500 server error
A 500 response means the server encountered an error while processing the request.
Inspect Laravel's application logs under:
storage/logs/
The actual filename depends on the configured logging channel.
Look for SQL exceptions, missing tables, incorrect model names, or PHP errors.
For example:
SQLSTATE[42S02]: Base table or view not found
Usually means the expected table doesn't exist in the connected database.
Run:
php artisan migrate:status
Check whether the products migration was applied.
Don't assume every 500 error is caused by Vue. The browser is only reporting that Laravel failed to complete the request.
Vue displays nothing and no request is sent
If the Network tab doesn't show /api/products, the Vue component may not be mounting.
Check your app.js import:
import ProductsList from './components/ProductsList.vue';
Check the mounting instruction:
createApp(ProductsList).mount('#app');
Then confirm the Blade view contains:
<div id="app"></div>
Also inspect the browser Console tab for Vite compilation or module-loading errors.
If you're running locally, make sure Vite is running:
npm run dev
For a production build:
npm run build
Deploy the generated frontend assets with the Laravel application.
Fetch works locally but fails because of CORS
Cross-origin errors occur when the browser requests a different origin without the required permissions.
For example:
Vue frontend:
http://localhost:5173
Laravel backend:
http://127.0.0.1:8000
These are different origins.
If you host Vue separately, configure Laravel's CORS settings for the intended frontend origin and use the correct API URL.
For a browser request using credentials, the server also needs appropriate credentials and origin settings.
Don't disable browser security to make the request work.
The setup in this tutorial avoids the issue by serving the Vue page and API endpoint from the same Laravel origin.
The product list doesn't update after adding records
The component loads products when it mounts.
It won't automatically know that a different user added another product to MySQL.
That's expected.
The refresh button calls loadProducts() again and retrieves the latest records.
If you add product creation or editing forms later, call loadProducts() after a successful save.
For applications that need continuous updates, consider polling or a real-time broadcasting approach. Neither is necessary for a basic product listing.
Use pagination when the products table becomes large
The original controller uses:
->get();
That retrieves all matching records.
It's fine for a small example, but retrieving every row can become expensive as the table grows.
Laravel provides pagination through Eloquent.
Update the controller method:
public function index(): JsonResponse
{
$products = Product::query()
->select('id', 'name', 'price')
->orderBy('id')
->paginate(10);
return response()->json($products);
}
Now the endpoint returns a paginated response rather than a plain array.
The JSON contains the product records under data, alongside pagination metadata.
You'll need to change the Vue assignment from:
products.value = data;
To:
products.value = data.data;
Because the response structure has changed.
Also remove or adjust the earlier Array.isArray(data) check, since the top-level response is now an object.
For example, the relevant parsing code becomes:
const result = await response.json();
if (!Array.isArray(result.data)) {
throw new Error('Unexpected pagination response');
}
products.value = result.data;
Laravel also provides metadata such as the current page and last page, which you can use to build pagination controls.
The next page can be requested through:
fetch('/api/products?page=2')
I'd use Laravel's built-in pagination instead of writing manual SQL LIMIT and OFFSET handling for an ordinary product list.
For an existing project with custom routing or database issues, HeadsNTail services is available for hands-on development assistance.
FAQ
Can Vue connect directly to a MySQL database?
No. A browser-based Vue application shouldn't open a direct MySQL connection.
Database credentials and server-side queries belong in Laravel. Vue should communicate with Laravel through an HTTP endpoint that returns only the data the user is authorized to access.
How do I fetch data from Laravel API using Axios in Vue 3?
Install Axios:
npm install axios
Then use it inside a Vue component:
import axios from 'axios';
import { ref, onMounted } from 'vue';
const products = ref([]);
onMounted(async () => {
try {
const response = await axios.get('/api/products');
products.value = response.data;
} catch (error) {
console.error('Failed to load products:', error);
}
});
Axios automatically parses JSON responses in ordinary cases.
The existing Laravel controller doesn't need to change. This example assumes the endpoint returns a plain array, not a paginated response.
How do I display data from a Laravel database in a Vue table?
Fetch the records from a Laravel endpoint and store them in a reactive array.
Then use v-for to generate the table rows:
<tr v-for="product in products" :key="product.id">
<td>{{ product.name }}</td>
<td>{{ product.price }}</td>
</tr>
Vue will update the table whenever the reactive array changes.
How do I get a single database record in Vue using Laravel?
Create a Laravel route that accepts a product ID:
Route::get('/api/products/{product}', function (
\App\Models\Product $product
) {
return response()->json(
$product->only(['id', 'name', 'price'])
);
});
Laravel route model binding retrieves the matching product or returns a 404 response if it doesn't exist.
Fetch the record from Vue:
const response = await fetch('/api/products/1', {
headers: {
Accept: 'application/json',
},
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const product = await response.json();
For private records, add the appropriate authentication and authorization checks before returning them.
Why is Vue showing an empty array when MySQL has data?
First, open the Laravel API endpoint directly and inspect the JSON.
If Laravel returns [], check the Eloquent query and the database configured in .env. If Laravel returns records but Vue doesn't display them, check the component's response parsing, reactive state, and browser console.
For the next step after learning how to retrieve data from database using vue, add server-side search and pagination, then protect any endpoints that expose private data.
HNT-END


