Early of my carrier I started with Laravel and PHP native with no framework at all, with these technology I did't found such as CORS problem and never experience with that at all because it FullStack not separate between backend and frontend.
In 2023 I move to new company and applied as Frontend Developer, and build some project like company profile, marketing site and my team choose nuxt.js and backend was laravel.com. At the time I was integrate laravel, I use many nuxt module for example Nuxt Auth and this work well in nuxt 2, but when the requirement change this module was really hard to modify or maybe because of skill issue, I should finish multiple project at same dateline and no time to research for my own. Then I came to tradisional way to build auth with Cookies Js and somehow it working well but CORS problem came up and only happen when I do hard refresh the site.
Everytime I open the old project some CORS proplem still showing and this make me frustrated. Until I watch
The BEST way to proxy our API in Nuxt from
Alexander Lichter and found beautiful solution and never found CORS error in nuxt 3 since then.
Disclaimer this may not the best way for these case but i just share what I do from my experience.
server/api/[...].ts
import { FetchError } from 'ofetch'
import { joinURL } from 'ufo'
export default defineEventHandler(async (event) => {
// Laravel API
const proxyUrl = useRuntimeConfig(event).public.BASE_API
const path = event.path
try {
const targetUrl = joinURL(proxyUrl, path)
const headers = new Headers(event.headers)
headers.set('Accept', 'application/json')
headers.set('access-control-allow-credentials', 'true')
headers.set('access-control-allow-origin', proxyUrl)
return await proxyRequest(event, targetUrl, {
headers,
})
}
catch (error: any) {
console.error(error)
if (error instanceof FetchError) {
throw createError({
statusCode: error.response?.status || 422,
statusMessage: error.response?._data.message ?? 'Internal Server Error',
cause: error.response?._data ?? {},
data: error.response,
})
}
}
})
And for fetch on component was easy without any configuration no over engineering at all, and all endpoint just working well thanks to Alex.
<script lang="ts" setup>
const {data} =useFetch('/api/todo')
</script>
<template>
<pre>
{{ data }}
</pre>
</template>
CORS problem already done now. We move to authenticate section and I think auth also have complex logic behind for make it work especially you are working with SSR and you should make sure our user still authenticate while load website from server or user do hard refresh and so on, and I end up to create many composable, logic and make sure this work maybe spend time just for that but your timeline is not waiting you to finish the auth right?.
Finally I found nuxt module Nuxt Auth Utils created by Sébastien Chopin who initial the Nuxt Js. I have interested with this module when Sébastien announced back then. Before I watch Alex video on YouTube I have no idea how to use this module and when reading on the docs it was like everything in single Nuxt App without external API. Until this section I try think again from beginning I face CORS and now auth, I think how the logic working across app and how many line should I wrote.
Until I read again the nuxt auth utils docs and found Session Management
section and I have idea to experimental with nuxt auth utils in my project:
I create separate file on my server folder to prevent long line
server/api/sign-in.ts
import { FetchError } from 'ofetch'
import { joinURL } from 'ufo'
export default defineEventHandler(async (event) => {
const proxyUrl = useRuntimeConfig(event).public.BASE_API
const body = await readBody(event)
try {
// pass the email and password to external API (laravel in this case)
const response = await $fetch<{ token: string, profile: any }>(joinURL(proxyUrl, '/api/login'), {
headers: {
'access-control-allow-credentials': 'true',
'access-control-allow-origin': proxyUrl,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
method: 'POST',
body: {
email: body.email,
password: body.password,
},
})
if (response) {
// token from API set in internal auth utils logic
await setUserSession(event, {
user: response.profile,
maxAge: 60 * 60 * 24 * 7, // 1 week
secure: {
token: response.token,
},
loginAt: new Date().toISOString(),
})
}
}
catch (error: any) {
if (error instanceof FetchError) {
throw createError({
statusCode: error.response?.status || 422,
statusMessage: error.response?._data?.message || 'Internal Server Error',
data: error.response?._data ?? {},
cause: error,
})
}
console.error('FetchError', error)
}
})
for route /api/sign-in I just make this separate from server/api/[...].ts to easy for debugging not fancy code here just catch body request from client site to nuxt server and pass it to API endpoint. At this point my external API just return token and user data if success nothing more. Then those data was passing to setUserSession composable from nuxt auth utils. Now auth API token was not expose to public and only session from nuxt auth utils.
Regarding the docs anything in secure section was not expose to public like user data profile. In the next section we will see how to use this token for API.
{
secure:{
token:'my-token-from-external-api'
}
}
Now we create component for authenticate our app
<script lang="ts" setup>
const state=ref({
email:"",
password:""
})
async function onSubmit(){
try {
// laravel require csrf-cookie for authenticate
// ignore this if you have different way
// optional
await $fetch('/api/sanctum/csrf-cookie', {
method: 'GET',
credentials: 'include',
})
await $fetch('/api/sign-in',{
method:"POST",
body:{
email:state.value.email,
password:state.value.email
}
})
useRouter().push('/secret')
} catch(e){
// your logic here
}
}
</script>
<template>
<form @submit:prevent="onSubmit"></form>
</template>
Until here our authenticate was running well, next we modify our server/api/[...].ts
import { FetchError } from 'ofetch'
import { joinURL } from 'ufo'
export default defineEventHandler(async (event) => {
const proxyUrl = useRuntimeConfig(event).public.BASE_API
const session = await getUserSession(event)
const path = event.path
try {
const targetUrl = joinURL(proxyUrl, path)
const headers = new Headers(event.headers)
headers.set('Accept', 'application/json')
headers.set('access-control-allow-credentials', 'true')
headers.set('access-control-allow-origin', proxyUrl)
// Set authorization header from secure auth utils data like I mention before
if (session.secure?.token) {
headers.set('Authorization', `Bearer ${session.secure.token}`)
}
// optional only for laravel
if (path === '/api/sanctum/csrf-cookie') {
// laravel API not setup csrf-cookie endpoint to API
const newPath = event.path.replace(/^\/api\//, '')
const path = joinURL(proxyUrl, newPath)
return await proxyRequest(event, path, {
headers,
})
}
return await proxyRequest(event, targetUrl, {
headers,
})
}
catch (error: any) {
console.error(error)
if (error instanceof FetchError) {
if (error.response?.status === 401) {
// auto logout when response status 401
await clearUserSession(event)
await sendRedirect(event, '/', 301)
}
// other error return to client
throw createError({
statusCode: error.response?.status || 422,
statusMessage: error.response?._data.message ?? 'Internal Server Error',
cause: error.response?._data ?? {},
data: error.response,
})
}
}
})
With all configuration above all authenticate route for your backend just working well as long our app still authenticate.
Create a middleware for our app.
app/middleware/auth.ts
export default defineNuxtRouteMiddleware(async (_from, _to) => {
// check for user should login
const { loggedIn } = useUserSession()
if (!loggedIn.value) {
return navigateTo('/')
}
})
Implement middleware in our secret page
app/pages/secret
<script lang="ts" setup>
definePageMeta({
middleware:['auth']
})
</script>
<template>
<div>secret page</div>
</script>
After perform login and all work well will navigate us to secret page. Now the last part is keep our profile sync with our backend and sometimes logout should call API logout to our backend. thank's to nuxt auth utils we can extend our nuxt auth utils .Extend Session was very helpful for this case
server/plugins/sessions.ts
import { FetchError } from 'ofetch'
export default defineNitroPlugin(() => {
sessionHooks.hook('fetch', async (session, event) => {
const proxyUrl = useRuntimeConfig(event).public.BASE_API
if (!session.secure?.token) {
return
}
try {
// sometimes we change the profile or admin change our profile data and we want effect to current login API immediate.
const data = await $fetch('/api/profile', {
baseURL: proxyUrl,
headers: {
'access-control-allow-credentials': 'true',
'access-control-allow-origin': proxyUrl,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': session.secure ? `Bearer ${session?.secure.token}` : '',
},
})
// we replace the user data from login session
await replaceUserSession(event, {
user: data?.data,
})
}
catch (error) {
if (error instanceof FetchError) {
if (error.response?.status === 401) {
await clearUserSession(event)
await sendRedirect(event, '/', 301)
}
}
console.error(error)
}
})
sessionHooks.hook('clear', async (session, event) => {
const proxyUrl = useRuntimeConfig(event).public.BASE_API
if (!session.secure?.token) {
return
}
// here is you can sent logout to our API before it clear the internal authenticate data
try {
await $fetch('/api/logout', {
baseURL: proxyUrl,
method: 'POST',
headers: {
'access-control-allow-credentials': 'true',
'access-control-allow-origin': proxyUrl,
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': session.secure ? `Bearer ${session?.secure.token}` : '',
},
})
}
catch (error) {
console.error(error)
}
})
})
<script setup>
const { loggedIn, user, session, fetch, clear } = useUserSession()
</script>
<template>
<div v-if="loggedIn">
<h1>Welcome {{ user.login }}!</h1>
<p>Logged in since {{ session.loggedInAt }}</p>
<button @click="clear">Logout</button>
<button @click="fetch">Update your session</button>
</div>
</template>
After use this pattern CORS is gone and auth was smooth in our nuxt app and now we only focus how to build features.
If you have an idea to improve this article please share with me on the comment
Silvester Wali

Top comments (0)