Skip to content

Laravel Breeze API installation + Vuero connection

This is a tutorial, made by Denis Neagoe, for setting up a REST API endpoint for your Vuero project.

Everything in this page can be found on the Laravel official docs:

Laravel Breeze API Installation

  1. In order for you to start you will need Composer and you can download it from here: https://getcomposer.org/download/ (if the composer command doesn't work after your installation, you will have to manually add the envirnoment variable - windows: https://docs.oracle.com/en/database/oracle/machine-learning/oml4r/1.5.1/oread/creating-and-modifying-environment-variables-on-windows.html#GUID-DD6F9982-60D5-48F6-8270-A27EC53807D0)
  2. Proceeding, if you didn't encounter any problem you will need to install the Laravel installer using composer global require laravel/installer
  3. After installing the Laravel installer you will need to close and open again your terminal and check if the laravel command works by using it
  4. If everything went as planned you can now run laravel new backend and wait for the application to be created
  5. After running the command above, you will need to create a database either in MySQL, MongoDB, PostgreSQL, etc. in order to have it ready for the next steps
  6. By now you will have a complete Laravel default application so you will need to install Breeze by using composer require laravel/breeze --dev. Running the composer command will add Breeze to your composer packages and you will be ready for the next step
  7. In order for the API base to be create, you will have to run the artisan command php artisan breeze:install api. This command will remove all your views files and other stuff that would normally be needed in a basic Laravel app.
  8. The Breeze API installation will add an environment variable called FRONTEND_URL, which by default will point to: http://localhost:3000, which is the port of the front-end development server (Vuero uses it by default)
  9. After installing the Breeze API you will need to run your new migrations using php artisan migrate
  10. Now you will need to serve your application with the artisan command php artisan serve and your application will be up and running

Vuero changes

  1. You can now delete the json-server dependency from your packages.json as you won't need it anymore since you've create a new backend application
  2. Head into your .env file and change the VITE_API_BASE_URL to VITE_API_BASE_URL=http://localhost:8000 or simply edit the port to 8000 as it's the default port Laravel serves on
  3. Now you will have your apps connected through the 8000 and 3000 ports

API composable changes

In order for your Vuero application to work you can create useLaravelFetch composable with ofetch:

Create src/composable/useLaravelFetch.ts and add the following code:

ts
import { ofetch } from 'ofetch'

import { useUserSession } from '/@src/stores/userSession'

export function useLaravelFetch() {
  const userSession = useUserSession()

  return ofetch.create({
    baseURL: import.meta.env.VITE_API_BASE_URL ?? 'http://localhost:8000',
    credentials: 'include',
    headers: {
      'Content-Type': 'application/json',
      'X-Requested-With': 'XMLHttpRequest',
    },
    // We set an interceptor for each request to
    // include Bearer token to the request if user is logged in
    onRequest: ({ options }) => {
      if (userSession.isLoggedIn) {
        options.headers = {
          ...options.headers,
          Authorization: `Bearer ${userSession.token}`,
        }
      }
    },
  })
}

This function will set your default endpoint to the one in the .env file and will add the X-Requested-With and Content-Type headers as well as the credentials: 'include' property.

Catching back-end errors

In order to catch back-end errors we will make a new file composable called useLaravelError and we will add it into src/composable/useLaravelError.ts with the following code:

ts
export const useLaravelError = (err: any) => {
  if (err.response?.status === undefined) {
    if (err.message == 'Network Error') return 'Connection error! Please try again later'
    else return err.message
  }

  let message
  switch (err.response?.status) {
    case 401:
      message = 'auth.errors.unauthenticated'

      break
    case 403:
      message = "You don't have the permission to see this page!"

      break
    case 422:
      message = Object.values<string>(err.response.data?.errors)?.flat()?.[0]

      break
    case 500:
      message = 'Internal error! Please try again later'

      break
  }
  return message
}

Catching field errors

As you will see below, in order to catch field errors and display them, we will need a wrapper for the setFieldError function. To create the wrapper, head to /src/utils/api/ and create a new file called catchFieldError.ts and add the following code:

ts
export function catchFieldError(err: any, setFieldError: any) {
  if (err.response?.status === 422) {
    const errors = err.response.data?.errors
    for (const field in errors) {
      setFieldError(field, errors[field][0])
    }
  }
}

The previous function will iterate through your field errors and it will add the error message under each specific field.

UserSession store

Since you've changed the way everything is handled, you'll have to make a few changes to the UserSession store, handled by Pinia. In my case I changed the useUserSession composable to the code below, but it can be changed to fit your personal needs:

ts
export interface User {
  id: number
  username: string
  email: string
}

import { acceptHMRUpdate, defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useStorage } from '@vueuse/core'

import type { User } from '/@src/models/user'

export const useUserSession = defineStore('userSession', () => {
  // token will be synced with local storage
  // @see https://vueuse.org/core/usestorage/
  const token = useStorage('token', '')

  const user = ref<Partial<User>>()
  const loading = ref(true)

  const isLoggedIn = computed(() => token.value !== undefined && token.value !== '')

  function setUser(newUser: Partial<User>) {
    user.value = newUser
  }

  function setToken(newToken: string) {
    token.value = newToken
  }

  function setLoading(newLoading: boolean) {
    loading.value = newLoading
  }

  async function logoutUser() {
    token.value = undefined
    user.value = undefined
  }

  return {
    user,
    token,
    isLoggedIn,
    loading,
    logoutUser,
    setUser,
    setToken,
    setLoading,
  } as const
})

/**
 * Pinia supports Hot Module replacement so you can edit your stores and
 * interact with them directly in your app without reloading the page.
 *
 * @see https://pinia.esm.dev/cookbook/hot-module-replacement.html
 * @see https://vitejs.dev/guide/api-hmr.html
 */
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserSession, import.meta.hot))
}

Note that We used Partial for the data type because in my case I had more fillables than the ones I put in the snippet so take into consideration that you will probably need to change it.

Login page changes

As of now we have connected both the Laravel and Vuero apps so we can change the login method too. I made a few changes to the original file and I imported yup for data validation and We used vue-i18n for the translations so you will need to create them inside of the JSON file.

Head to /src/pages/auth/login.vue and change your login function to:

ts
import { useI18n } from 'vue-i18n'

import { toFormValidator } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { z as zod } from 'zod'

import { useDarkmode } from '/@src/stores/darkmode'
import { useNotyf } from '/@src/composable/useNotyf'
import { useLaravelError } from '/@src/composable/useLaravelError'
import { useLaravelFetch } from '/@src/composable/useLaravelFetch'

import { useUserSession } from '/@src/stores/userSession'
import { authenticateUser } from '/@src/services/modules/user/account'

import { catchFieldError } from '/@src/utils/api/catchFieldError'

const darkmode = useDarkmode()
const router = useRouter()
const route = useRoute()
const notyf = useNotyf()
const userSession = useUserSession()
const redirect = route.query.redirect as string

const isLoading = ref(false)
const { t } = useI18n()

const $fetch = useLaravelFetch()

// Define a validation schema
const validationSchema = toFormValidator(
  zod.object({
    email: zod
      .string({
        required_error: t('auth.errors.email.required'),
      })
      .email(t('auth.errors.email.format')),
    password: zod.string({
      required_error: t('auth.errors.password.required'),
    }),
    remember: zod.boolean().optional(),
  })
)

const { handleSubmit, isSubmitting, setFieldError } = useForm({
  validationSchema,
  initialValues: {
    email: '',
    password: '',
    remember: false,
  },
})

const onLogin = handleSubmit(async (values) => {
  if (!isLoading.value) {
    isLoading.value = true

    try {
      await $fetch('/sanctum/csrf-cookie')
      await $fetch('/login', {
        method: 'POST',
        body: values,
      })

      const user = await $fetch('/api/user')

      userSession.setUser(user)

      if (redirect) {
        router.push(redirect)
      } else {
        router.push('/')
      }

      notyf.dismissAll()
      notyf.success(`${t('auth.logged-in')}, ${userSession.user!.username}`)
    } catch (err: any) {
      catchFieldError(err, setFieldError)
      notyf.error(useLaravelError(err))
    } finally {
      isLoading.value = false
    }
  }
})

useHead({
  title: 'Login - Vuero',
})

Register page changes

The registration process is similar to the login process:

Head to src/pages/auth/register.vue and change your register function to:

ts
import { useI18n } from 'vue-i18n'

import { toFormValidator } from '@vee-validate/zod'
import { useForm } from 'vee-validate'
import { z as zod } from 'zod'

import { useDarkmode } from '/@src/stores/darkmode'
import { useNotyf } from '/@src/composable/useNotyf'
import { useLaravelError } from '/@src/composable/useLaravelError'
import { useLaravelFetch } from '/@src/composable/useLaravelFetch'

import { useUserSession } from '/@src/stores/userSession'
import { authenticateUser } from '/@src/services/modules/user/account'

import { catchFieldError } from '/@src/utils/api/catchFieldError'

const darkmode = useDarkmode()
const router = useRouter()
const route = useRoute()
const notyf = useNotyf()
const userSession = useUserSession()
const redirect = route.query.redirect as string

const isLoading = ref(false)
const { t } = useI18n()

const $fetch = useLaravelFetch()

const validationSchema = toFormValidator(
  zod
    .object({
      username: zod
        .string({
          required_error: t('auth.errors.username.required'),
        })
        .min(1, t('auth.errors.username.required')),
      email: zod
        .string({
          required_error: t('auth.errors.email.required'),
        })
        .email(t('auth.errors.email.format')),
      password: zod
        .string({
          required_error: t('auth.errors.password.required'),
        })
        .min(8, t('auth.errors.password.length')),
      password_confirmation: zod.string({
        required_error: t('auth.errors.password_confirmation.required'),
      }),
    })
    .refine((data) => data.password === data.password_confirmation, {
      message: t('auth.errors.password_confirmation.match'),
      path: ['password_confirmation'],
    })
)

const { handleSubmit, isSubmitting, setFieldError } = useForm({
  validationSchema,
  initialValues: {
    username: '',
    email: '',
    password: '',
    password_confirmation: '',
  },
})

const onRegister = handleSubmit(async (values) => {
  if (!isLoading.value) {
    isLoading.value = true

    try {
      await $fetch('/sanctum/csrf-cookie')
      await $fetch('/register', {
        method: 'POST',
        body: values,
      })

      const user = await $fetch('/api/user')

      userSession.setUser(user)

      if (redirect) {
        router.push(redirect)
      } else {
        router.push('/')
      }

      notyf.dismissAll()
      notyf.success(`${t('auth.registered')}, ${userSession.user!.username}`)
    } catch (err: any) {
      catchFieldError(err, setFieldError)
      notyf.error(useLaravelError(err))
    } finally {
      isLoading.value = false
    }
  }
})

useHead({
  title: 'Register - Vuero',
})

And this is an example of how a field should look like for both authentication pages (using login-1 and signup-1 templates):

vue
<template>
	<VField id="email" v-slot="{ field }">
		<VControl>
			<VInput type="email" class="input" :placeholder="t('auth.placeholder.email')" autocomplete="email" :disabled="isSubmitting" />
			<div class="auth-label">{{ t('auth.placeholder.email') }}</div>
			<div class="autv-icon">
				<i aria-hidden="true" class="lnil lnil-envelope"></i>
			</div>
			<p v-if="field?.errors?.value?.length" class="help is-danger">{{ field.errors?.value?.join(', ') }}</p>
		</VControl>
	</VField>
</template>

Notes

  • Every time you will need to work on your Vuero application or Laravel application you will need to run the artisan command php artisan serve
  • In order for any of your requests to work you will need to make an initial call to the /sanctum/csrf-cookie route because this will inject the CSRF token into your headers. Without a CSRF token you will receive an error looking like this: CSRF Token mismatch
  • Remember to always use await $fetch('/sanctum/csrf-cookie') before your requests.

All Rights Reserved