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

Environment Base URL

.env:

env
VITE_API_URL=http://127.0.0.1:8000

src/api/client.ts:

typescript
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:

typescript
export default defineConfig({
  plugins: [vue()],
  server: {
    proxy: {
      "/api": {
        target: "http://127.0.0.1:8000",
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ""),
      },
    },
  },
});

Frontend calls:

typescript
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:

python
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-CORSFlask chapter.

Django: django-cors-headersDjango 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

typescript
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)

bash
npm install axios

Interceptors centralize auth and error logging—fetch is enough for learning.

Error Handling Tips

StatusUI action
400Show validation message from body
401Logout + redirect login
403Permission denied message
404Not found state
500Generic error + retry button

Parse FastAPI validation errors:

typescript
type ApiError = { detail?: string | { msg: string }[] };

Practice Exercise

  1. Configure Vite proxy to a local FastAPI or Flask API
  2. List resources in a component with loading/error UI
  3. 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.