DEV Community

Cover image for Laravel API Integration in Nuxt.js
syed kamruzzaman
syed kamruzzaman

Posted on

Laravel API Integration in Nuxt.js

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 screenshot 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/

Enter fullscreen mode Exit fullscreen mode

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 block:

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:
Configure AI Prompt : Part-2

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;

Enter fullscreen mode Exit fullscreen mode

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);
      }
    }





  }
});

Enter fullscreen mode Exit fullscreen mode

# 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 }
  })
Enter fullscreen mode Exit fullscreen mode

# 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;
    },
  },
});

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

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>

Enter fullscreen mode Exit fullscreen mode

With that, we've completed the home page, which should look like this:

Image description

# 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>

Enter fullscreen mode Exit fullscreen mode

Image description

# 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>

Enter fullscreen mode Exit fullscreen mode

Image description

# 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>

Enter fullscreen mode Exit fullscreen mode

Image description

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

All Episodes
Creating the API # [Tutorial-1]
Configure AI Prompt # [Tutorial-2]
Designing the UI # [Tutorial-3]
Setting up on an Linux Server # [Tutorial-4]

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

Top comments (0)