Fetching API and CORS
Introduction
Vue apps load data from REST APIs with fetch or axios. In development, the Vite server often runs on port 5173 while FastAPI or Flask runs on another port—browsers enforce CORS. This chapter covers env-based URLs, Vite proxy, request patterns, loading/error UI, and JWT headers.
Prerequisites
- Composables
- Lifecycle Hooks
- Promises in JavaScript
- A running API or public test endpoint
Environment Base URL
.env:
VITE_API_URL=http://127.0.0.1:8000src/api/client.ts:
const baseUrl = import.meta.env.VITE_API_URL ?? "";
export async function apiGet<T>(path: string): Promise<T> {
const res = await fetch(`${baseUrl}${path}`, {
headers: { Accept: "application/json" },
});
if (!res.ok) {
throw new Error(`Request failed: ${res.status}`);
}
return res.json() as Promise<T>;
}Code explanation:
- Only
VITE_*vars are exposed to client code - Never put server secrets in
VITE_variables—they ship to the browser
Vite Dev Proxy (Same-Origin in Dev)
Avoid CORS during local dev by proxying /api to backend:
vite.config.ts:
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
"/api": {
target: "http://127.0.0.1:8000",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});Frontend calls:
const posts = await apiGet<Post[]>("/api/v1/posts");Browser sees http://localhost:5173/api/...—same origin; Vite forwards to FastAPI.
Compare Flask CORS and Frontend when proxy is not used.
CORS on the Backend
When SPA and API are on different origins in dev or production, API must send CORS headers.
FastAPI:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173", "http://127.0.0.1:5173"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)Flask: Flask-CORS — Flask chapter.
Django: django-cors-headers — Django API auth and CORS.
Production: restrict allow_origins to your real frontend domain—not * with credentials.
useApi Composable
src/composables/useApi.ts:
POST JSON
Use in form submit handlers—Forms and v-model.
JWT Authorization
import { storeToRefs } from "pinia";
import { useAuthStore } from "@/stores/auth";
const { token } = storeToRefs(useAuthStore());
await fetch("/api/v1/me", {
headers: {
Authorization: `Bearer ${token.value}`,
},
});On 401, clear auth and redirect to login—Router guards. Backend JWT details: FastAPI authentication.
Three-State UI
Always handle loading, error, and empty states— not only success.
axios (Optional)
npm install axiosInterceptors centralize auth and error logging—fetch is enough for learning.
Error Handling Tips
| Status | UI action |
|---|---|
| 400 | Show validation message from body |
| 401 | Logout + redirect login |
| 403 | Permission denied message |
| 404 | Not found state |
| 500 | Generic error + retry button |
Parse FastAPI validation errors:
type ApiError = { detail?: string | { msg: string }[] };Practice Exercise
- Configure Vite proxy to a local FastAPI or Flask API
- List resources in a component with loading/error UI
- POST a new item and refresh the list
See Blog Reader Project.
FAQ
CORS error in browser?
Backend missing CORS headers or wrong origin—or use Vite proxy in dev.
credentials: 'include'?
Needed for cookie sessions; requires explicit CORS allow_credentials.
Why not call API from SSR without CORS?
Server-to-server has no browser CORS—only browser SPAs hit CORS.
Double fetch on mount?
Strict Mode in React double-mounts; Vue 3 dev tools less common—still avoid duplicate calls in watch + onMounted.
OpenAPI / Swagger?
FastAPI /docs helps backend contract—FastAPI OpenAPI.
Mock API without backend?
Use msw or JSONPlaceholder for UI-first development.