Fixing CORS errors in Vue and Nuxt: dev proxies, server routes and more
How to fix CORS errors in Vue and Nuxt: Vite's dev proxy for Vue, Nuxt server routes, routeRules proxies and Nitro devProxy, plus what works on static hosting.
Vue and Nuxt apps run into CORS errors the same way every frontend does: the browser on http://localhost calls an API on another origin, and the API doesn’t send Access-Control-Allow-Origin. What differs is the toolbox. A plain Vue app has only Vite’s dev server, while Nuxt has a full server layer, Nitro, that can make the problem disappear in production too. This guide shows how to fix CORS errors in Vue and Nuxt, and which fixes survive deployment.
First, confirm it’s really CORS
Open the browser console. A CORS failure says the request “has been blocked by CORS policy” (Chrome) or “Cross-Origin Request Blocked” (Firefox). In your code, fetch, $fetch and Axios all report it as a generic network error with no status. If the console wording is different, for example a CSP “Refused to connect” message, you have a different problem. Our DevTools debugging guide shows how to tell them apart.
Remember where the fix lives: only the server that sends the response can allow your origin. Vue can’t. What Vue and Nuxt can do is change who makes the request.
Vue with Vite: the dev server proxy
A Vue 3 app created with create-vue runs on Vite, so it uses Vite’s server.proxy. Requests to a path on the dev server are forwarded server-side, and the browser only sees same-origin requests to localhost:
// vite.config.ts
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
});
Then call the relative path from a component:
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const items = ref<{ id: number; name: string }[]>([]);
onMounted(async () => {
const response = await fetch('/api/v1/items');
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
items.value = await response.json();
});
</script>
<template>
<ul>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>
changeOrigin: true sets the Host header to the target’s host, which most servers behind a CDN require. Older projects on Vue CLI have the equivalent devServer.proxy option in vue.config.js.
The catch: it’s development only
vite build outputs static files. There’s no dev server in production, so /api hits your static host and returns a 404, or your index.html if the host has a single-page-app fallback. That’s the source of the confusing Unexpected token '<' JSON errors that only appear after deploying. The React and Vite guide covers the same problem in more depth, and the production fixes later in this article apply to plain Vue apps too.
Nuxt: let the server make the call
Nuxt runs a server (Nitro) alongside your app, both in development and in production when you deploy as a server or serverless app. That changes everything: your Vue components can call your own Nuxt server on the same origin, and Nitro calls the API. Servers aren’t subject to CORS.
Option 1: a server route
Create a route in server/api/. It runs only on the server:
// server/api/items.get.ts
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig(event);
return await $fetch('https://api.example.com/v1/items', {
headers: { Authorization: `Bearer ${config.apiSecret}` },
});
});
Declare the secret in nuxt.config.ts. Top-level runtimeConfig values stay on the server; only those under public reach the browser:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: '', // set NUXT_API_SECRET in the environment
public: {
siteName: 'My app',
},
},
});
In a component, call your own endpoint:
<script setup lang="ts">
const { data: items, error } = await useFetch('/api/items');
</script>
This is the best fix when the API needs a secret key, because the key never leaves the server. It’s also the right place to trim the response to what the page needs, cache it, or combine several upstream calls.
Option 2: a routeRules proxy
When you just want to forward a path to an API without writing a handler, Nuxt’s route rules can proxy it:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/api/**': { proxy: 'https://api.example.com/**' },
},
});
A request to /api/v1/items on your Nuxt app is forwarded to https://api.example.com/v1/items. It works in development and in server deployments, and needs no code. You can’t add a secret header or transform the response here; use a server route for that. Inside a server route, h3’s proxyRequest(event, url) gives you a programmable middle ground.
Option 3: Nitro devProxy, for development
If the API will be same-origin in production anyway, for example served by the same reverse proxy, you may only need forwarding during development:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
devProxy: {
'/api': { target: 'https://api.example.com', changeOrigin: true },
},
},
});
devProxy only exists in development, just like Vite’s server.proxy.
Forwarding the visitor’s context
Once Nitro makes the call, the upstream API sees your server, not the visitor. That’s usually what you want, but two details matter. Forward only the headers the API needs, rather than copying every incoming header, so cookies and internal headers don’t leak upstream. And if the API rate-limits by IP address, every visitor now shares your server’s address. Check whether the API offers a way to pass the end user’s identity, or apply your own per-user limits in the server route.
The static-hosting trap
Server routes and route-rule proxies need Nitro running. If you deploy with nuxt generate to purely static hosting, there’s no server at request time. Routes under /api won’t exist after deployment, apart from responses that were prerendered at build time. Check which mode you deploy in before choosing a fix:
| Deployment | Server routes | routeRules proxy | devProxy |
|---|---|---|---|
nuxt dev | Yes | Yes | Yes |
Server or serverless (nuxt build) | Yes | Yes | No |
Static (nuxt generate) | Only prerendered responses | No | No |
Fixes for static Vue and Nuxt sites
When there’s no server, the browser has to call the API itself, so the API or something in front of it must send CORS headers:
- Configure the API, if you own it, to allow your site’s origin and send
Vary: Origin. Remember the preflight for JSON andAuthorizationrequests. - Add a rewrite on your host that serves the API under your own origin, if your static host supports proxy rewrites.
- Use a serverless function for anything that needs a secret.
- Use a CORS proxy for third-party APIs you can’t configure.
For the last case, ProxifyEdge works from any static Vue or Nuxt site:
const target = 'https://api.example.com/v1/items';
const items = await $fetch(`https://api.proxifyedge.com/proxy?url=${encodeURIComponent(target)}`, {
headers: { 'X-API-Key': 'pk_your_public_key' },
});
A live key answers only the origins you list, so it’s safe to ship in the bundle. Use a test key while you work on localhost. If the upstream API needs a token, store it in the secrets vault instead of a public runtime config value. Start with the quick start.
Summary
- CORS errors come from the API, not from Vue. Fix them by changing who makes the request, or by making the API allow your origin.
- Plain Vue: Vite’s
server.proxyfixes development only. - Nuxt: server routes and
routeRulesproxies make calls from Nitro, so they work in production server deployments. Keep secrets in non-publicruntimeConfig. nuxt generatehas no server at request time. Static sites need CORS on the API, a host rewrite, a serverless function or a CORS proxy.
Related: how to fix ‘No Access-Control-Allow-Origin header is present’ and SvelteKit and Astro, which share Nuxt’s server-route approach.