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
- 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) - Proceeding, if you didn't encounter any problem you will need to install the Laravel installer using
composer global require laravel/installer
- 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 - If everything went as planned you can now run
laravel new backend
and wait for the application to be created - 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
- 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 - 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. - 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) - After installing the Breeze API you will need to run your new migrations using
php artisan migrate
- 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
- 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
- Head into your .env file and change the
VITE_API_BASE_URL
toVITE_API_BASE_URL=http://localhost:8000
or simply edit the port to8000
as it's the default port Laravel serves on - 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:
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:
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:
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:
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:
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:
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):
<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.