Laravel API Integration in Nuxt.js

syed kamruzzaman
10 min readOct 30, 2023

--

In this tutorial, we’ll integrate a Laravel API into a Nuxt.js application. Let’s dive in!

Step 1: Set Up Environment File
First, set up your environment file as shown in the block below:

OPEN_AI_API_KEY=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
API_BASE_URL=http://127.0.0.1:8000/api/
API_ROOT_URL=http://127.0.0.1:8000
VITE_API_URL=http://127.0.0.1:8000/api/

Step 2: Set Up Axios
While there are different ways to set up Axios, in this tutorial, we’ll use it as a helper function. Create a folder named helpers, and inside it, create a file named axios.js. Type the configuration as shown in the screenshot: [Screen Short]
We’ve already set up the necessary packages like Axios and Pinia. If you haven’t done this yet, follow the instructions in the link below:
[link]

import axios from 'axios';

const axiosInstance = axios.create({
baseURL: import.meta.env.VITE_API_URL,
headers: {
'Content-Type': 'application/json',
},
});

axiosInstance.interceptors.request.use(function (config) {
// Do something before request is sent
let token = localStorage.getItem("token");
config.headers["Authorization"] = "Bearer " + token;
return config;
});

export default axiosInstance;

Step 3: Set Up Pinia Store
Create the following files in your Pinia store:

  1. aiMovieStore.js
  2. categoryStore.js
  3. userStore.js

Now, add code to each file as shown in the block below:

userStore.js

import { defineStore } from 'pinia';
import axios from '../helpers/axios.js';


export const userStore = defineStore('userStore', {
state: () => ({
loading: false,
logInUserInfo: {},
categories: [],
isLogIn:false,
}),

getters: {
authCheck() {
if (process.client) {
if (this.isLogIn == true) {
return true;
}
if(this.isLogIn == false){
const token = localStorage.getItem('token');
return !!token;
}

return false;
}
},
token (){
if (typeof window == undefined) {
return false;
}
const token = localStorage.getItem("token") ?? this.logInUserInfo.data.token
if(token){
return token;
}

return null;
}
},

actions: {
async actionLogin(payload, token) {
try {
const response = await axios.post('/login', payload);
this.logInUserInfo = response.data;
this.isLogIn = true;
// Save the user data and token in local storage (for page reload)
localStorage.setItem('user', JSON.stringify(response.data.data));
localStorage.setItem('token', response.data.data.token);
navigateTo("/");
} catch (error) {
throw new Error('Login failed.', error);
}
},

async actionLogout(payload) {
const response = await axios.post('/logout');
localStorage.removeItem('user');
localStorage.removeItem('token');
this.logInUserInfo = null;
this.isLogIn = false;
},

async actionRegister(payload){
console.log('registation', payload)
try {
const response = await axios.post('/register', payload);
navigateTo("/login");
} catch (error) {
throw new Error('Registration failed.', error);
}
}





}
});

# categoryStore.js

import { defineStore } from 'pinia';
import { ref } from 'vue';
import axios from "../helpers/axios.js";

export const categoryStore = defineStore('categoryStore', () => {
const categories = ref([])

const actionStoreCategory = async(payload)=>{
console.log('actionStoreCategory', payload)
const formData = new FormData();
formData.append("name", payload.name);
formData.append("image", payload.file);
//console.log("actionManualMovieDataSendToServer", [...formData]);
const response = await axios.post("/category-store", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
navigateTo("/");
}

const actionAllCategoryApi = async()=> {
const config = useRuntimeConfig();
const data = await axios.get("/all-category");
console.log("axios data", data);
categories.value = data.data.data;
//this.actionAiMovieData = movieData;
}

return { actionStoreCategory, actionAllCategoryApi, categories }
})

# aiMovieStore.js

import axios from "../helpers/axios.js";

import { defineStore } from "pinia";

//define pinia store
export const aiMovieStore = defineStore("aiMovieStore", {

//initial store
state: () => ({
loading: false,
aiMovieData: {},
categories: [],
topMovies: [],
categoryWiseMovie: [],
}),

getters: {},

actions: {
async getToken() {
return await axios.get("/sanctum/csrf-cookie");
},

//here payload is ai data. this data store in pinia
async actionAiMovieData(payload) {
this.aiMovieData = payload;
this.loading = false;
},
async actionAllCategoryApi() {
const config = useRuntimeConfig();
const data = await axios.get("/all-category");
console.log("axios data", data);
this.categories = data.data.data;
//this.actionAiMovieData = movieData;
},

//here send ai data to server for save this data in our database
async actionAiMovieDataSendServer(payload) {
console.log("actionAiMovieDataSendServer", payload);
const response = await axios.post("/ai-movie-store", payload);
navigateTo("/");
},

//here menual movie data send to server for save this data in our database
async actionManualMovieDataSendToServer(payload) {
console.log("actionManualMovieDataSendToServer", payload);
const formData = new FormData();
formData.append("title", payload.title);
formData.append("description", payload.description);
formData.append("category_id", payload.category);
//formData.append('image', payload.file??"")
if (payload.file) {
formData.append("image", payload.file);
}
console.log("actionManualMovieDataSendToServer", [...formData]);
const response = await axios.post("/movie-store", formData, {
headers: {
"Content-Type": "multipart/form-data",
},
});
navigateTo("/");
},

//fetch top movies from database
async actionTopMovie(payload) {
const data = await axios.get("/top-movies");
this.topMovies = data.data.data.data;
},

//fetch category wise movie list from database
async actionCategoryWiseMovie() {
const data = await axios.get("/category-wise-movies");
console.log("actionCategoryWiseMovie", data);
this.categoryWiseMovie = data.data.data.data;
},
},
});

Step 4: Set Up App Pages

Go to the pages folder. You’ll find the following pages

1.index.vue
2.login.vue
3.registration.vue
4.manual-make-movie.vue

index.vue
This page includes three components:

  1. HeroSection
  2. TopMovies
  3. CategoryWiseMovies

In the TopMovies component, add the following code:

<template>
<section class="mt-9 bg-lime-300 dark:bg-rose-500 p-5 rounded-md">
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-700 text-base dark:text-white">Top Movies</span>
<div class="flex items-center space-x-2 fill-gray-500">
<NavigationArrow />
</div>
</div>

<div class="mt-4 grid grid-cols-2 sm:grid-cols-4 gap-x-5 gap-y-5">
<div class="flex flex-col rounded-xl overflow-hidden aspect-square border dark:border-zinc-600" v-for="topMovie in aiMOvieStoreInfo.topMovies" :key="topMovie">
<img :src="topMovie.image" class=" h-4/5 object-cover w-full " alt="">
<div class="w-full h-1/5 bg-white dark:bg-zinc-800 dark:text-white px-3 flex items-center justify-between border-t-2 border-t-red-600">
<span class="capitalize font-medium truncate">{{ topMovie.title }}</span>
<div class="flex space-x-2 items-center text-xs">
<IconImdbLogo />
<span>{{ getRandomRating(topMovie.id) }}</span>
</div>
</div>
</div>
</div>
</section>
</template>

<script setup>
import { aiMovieStore } from '@/store/aiMovieStore.js';
import IconImdbLogo from '../Icon/ImdbLogo.vue';
import NavigationArrow from '../Icon/NavigationArrow.vue';

const aiMOvieStoreInfo = aiMovieStore();
const randomRating = ref((Math.random() * 4 + 5).toFixed(1));
watchEffect(()=>{
aiMOvieStoreInfo.actionTopMovie();
},[])
const getRandomRating = (id) => {
const hashValue = id % 1000; // Use a large enough number for uniqueness, adjust as needed
const baseRating = (hashValue % 20 + 75) / 10; // Random number between 7.5 and 9.5
const randomRating = parseFloat((Math.random() * (10 - baseRating) + baseRating).toFixed(1));

return randomRating > 10 ? 10 : randomRating;
};

</script>

For the CategoryWiseMovies component, add this:

<template>
<section
class="mt-9 p-5 rounded-md"
v-for="category in aiMOvieStoreInfo.categoryWiseMovie"
:key="category"
:class="getRandomColorClass()"
>
<div class="flex items-center justify-between">
<span class="font-semibold text-gray-700 text-base dark:text-white">{{ category.name }}</span>
<div class="flex items-center space-x-2 fill-gray-500">
<NavigationArrow />
</div>
</div>

<div class="mt-4 grid grid-cols-2 gap-y-5 sm:grid-cols-3 gap-x-5 ">
<div class="flex flex-col rounded-xl overflow-hidden aspect-square border dark:border-zinc-600" v-for="movie in getRandomMovies(category.movies)" :key="movie">
<img :src="getImageUrl(movie.image)" class=" h-4/5 object-cover w-full " alt="">
<div class="w-full h-1/5 bg-white dark:bg-zinc-800 dark:text-white px-3 flex items-center justify-between border-t-2 border-t-red-600">
<span class="capitalize font-medium truncate">{{ movie.title }}</span>
<div class="flex space-x-2 items-center text-xs">
<IconImdbLogo />
<span>7.4</span>
</div>
</div>
</div>
</div>
</section>
</template>

<script setup>
import { aiMovieStore } from '@/store/aiMovieStore';
import IconImdbLogo from '../Icon/ImdbLogo.vue';
import NavigationArrow from '../Icon/NavigationArrow.vue';
const config = useRuntimeConfig();
const imageRootUrl = config.public.API_ROOT_URL

const aiMOvieStoreInfo = aiMovieStore();
onMounted(()=>{
aiMOvieStoreInfo.actionCategoryWiseMovie()
})

// For make random color
const getRandomColorClass = () => {
const randomIndex = Math.floor(Math.random() * colors.length);
return colors[randomIndex];
};
const colors = [
'bg-red-400',
'bg-blue-400',
'bg-green-400',
'bg-yellow-400',
'bg-pink-400',
'bg-blue-800',
'bg-black',

];
//image generat url
const getImageUrl = (thumbnail) => {
const imageUrl = thumbnail ? thumbnail.replace('public', 'storage') : '';
return `${imageRootUrl}/${imageUrl}`;
};

//random 3 movies generate
const getRandomMovies = (movies) => {
if (movies.length <= 3) {
return movies;
} else {
const shuffledMovies = [...movies].sort(() => Math.random() - 0.5);
return shuffledMovies.slice(0, 3);
}
};
</script>
Home Page

# registration.vue Page
Add the following lines:

<template>
<div class="flex h-screen items-center justify-center bg-gray-100">
<div class="bg-white p-8 shadow-md rounded-lg w-80">
<h1 class="text-2xl font-semibold mb-4">Register</h1>
<form @submit.prevent="register">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Name</label>
<input v-model="userInfo.name" type="text" class="mt-1 block w-full border rounded-md px-3 py-2" required />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Email</label>
<input v-model="userInfo.email" type="email" class="mt-1 block w-full border rounded-md px-3 py-2" required />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="userInfo.password" type="password" class="mt-1 block w-full border rounded-md px-3 py-2" required />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Confirm Password</label>
<input v-model="userInfo.password_confirmation" type="password" class="mt-1 block w-full border rounded-md px-3 py-2" required />
</div>
<button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Register</button>
</form>
</div>
</div>
</template>

<script setup>
import { userStore } from '@/store/userStore.js';
import { ref } from 'vue';

const userInfo = ref({
name:"",
email:"",
password:"",
password_confirmation:"",
})
const userStoreInfo = userStore()

const register = async ()=>{
if(userInfo.value.password != userInfo.value.password_confirmation){
alert('Password does not match, please check' );
return;
}

await userStoreInfo.actionRegister(userInfo.value)

}

</script>
Registration Page

# login.vue page

<template>
<div class="flex h-screen items-center justify-center bg-gray-100">
<div class="bg-white p-8 shadow-md rounded-lg w-80">
<h1 class="text-2xl font-semibold mb-4">Login </h1>
<form @submit.prevent="login">
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Email</label>
<input v-model="loginInfo.email" type="email" class="mt-1 block w-full border rounded-md px-3 py-2 text-red-800" required />
</div>
<div class="mb-4">
<label class="block text-sm font-medium text-gray-700">Password</label>
<input v-model="loginInfo.password" type="password" class="mt-1 block w-full border rounded-md px-3 py-2 text-red-800" required />
</div>
<button type="submit" class="bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600">Login</button>
</form>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue';
import { userStore } from '../store/userStore.js';

const userStoreInfo = userStore();
const loginInfo = ref({
email:'',
password:''
})

const login = async ()=>{

if(!loginInfo.value.email || !loginInfo.value.password){
alert('Please Type input field')
}

await userStoreInfo.actionLogin(loginInfo.value,)

}
</script>
Login Page

# manual-make-movie.vue

In this page, navigate to the FormInput component and add the following line:

<template>
<div class="bg-gray-200 p-5 rounded-md dark:bg-black">

<div class="text-center mb-8 relative">
<h1 class="text-2xl font-bold text-gray-700 mb-3 dark:text-white">Manual Movie Maker</h1>
<img src="/assets/pictures/storytelling-08.gif" alt="" class="rounded-full w-48 h-48 absolute -top-5 right-0 sm::hidden">
</div>

<div class="mx-10 px-10 py-16 bg-gray-400 dark:bg-black dark:border rounded-md">
<div class="mb-6">
<label for="large-input" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Movie
Title
</label>
<input type="text" id="large-input"
class="block w-full p-4 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Type your Movie Title..." v-model="movieData.title">
<p v-if="!movieData.title && isSubmitted" class="text-red-500 mt-2">Movie Title is required.</p>
</div>
<div class="mb-6">
<label for="base-input" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Movie
Category</label>
<select
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
v-model="movieData.category"
>
<option value="">Choose category</option>
<option :value="category.id" v-for="category in aiMovieStoreInfo.categories" :key="category">
{{ category.name }}
</option>
</select>
<p v-if="!movieData.category && isSubmitted" class="text-red-500 mt-2">Movie Category is required.</p>
</div>
<div class="mb-6">
<label for="message" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Movie Short
Description</label>
<textarea id="message" rows="4"
class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Type Movie short description..." v-model="movieData.description"></textarea>
<p v-if="!movieData.description && isSubmitted" class="text-red-500 mt-2">Movie Description is required.</p>
</div>
<div class="mb-6">
<label for="large-input" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">
Select Movie Poster
</label>
<input
type="file" id="large-input"
class="block w-full p-4 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-md focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
placeholder="Type your Movie Title..."
@change="handleFileChange"
>
<p v-if="!movieData.file && isSubmitted" class="text-red-500 mt-2">Movie Poster is required.</p>
</div>
<div class="text-right">
<button type="submit" class="bg-black text-white p-3 rounded-md dark:border" @click="submitForm">Make
Movie
</button>
</div>
</div>

</div>
</template>

<script setup>
import { ref } from 'vue';
import { aiMovieStore } from '../../store/aiMovieStore.js';

const movieData = ref({
title: "",
category: "",
description: "",
file:""
})

const aiMovieStoreInfo = aiMovieStore();
const isSubmitted = ref(false);

onMounted(()=>{
aiMovieStoreInfo.actionAllCategoryApi()
})

//input file handler
function handleFileChange(event) {
const file = event.target.files[0];
movieData.value.file = file;
}
//validation input
function validateInputs() {
const { title, category, description, file } = movieData.value;
if (!title || !category || !description || !file) {
//alert('Please fillup the form')
return false;
}
return true;
}

const submitForm = () => {
isSubmitted.value = true;
if (validateInputs()) {
aiMovieStoreInfo.actionManualMovieDataSendToServer(movieData.value)
}
};


</script>
Form Page

With that, our API integration is complete. The next step is to deploy this app on our Ubuntu server. Here’s the tutorial link for that:
[link]

Here is github link of this project
https://github.com/kamruzzamanripon/nuxt-movie-ui-with-laravel-api

That’s all. Happy Learning :) .
[if it is helpful, giving a star to the repository 😇]

All Episodes

Creating the API [Tutorial-1]:

https://medium.com/@rkamruzzaman/building-a-movie-portal-api-with-laravel-using-the-service-pattern-06595657f3d7

Configure AI Prompt [Tutorial-2]:

https://medium.com/@rkamruzzaman/a3e959349ca0

Designing the UI [Tutorial-3]:

https://medium.com/@rkamruzzaman/93e96a5566b1

Setting up on an Linux Server [Tutorial-4]:

https://medium.com/@rkamruzzaman/a50c8c57776d

--

--

syed kamruzzaman

I'm a Frontend and Backend developer with a passion for PHP and JavaScript, along with expertise in JavaScript-related libraries.