CORS and Frontend Integration
Introduction
Single-page apps (React, Vue, Svelte) often run on localhost:5173 while Flask API runs on localhost:5000. Browsers block cross-origin requests unless the API sends CORS headers. This chapter configures Flask-CORS, structures /api/v1/ routes, and notes OpenAPI documentation tools for API teams.
Prerequisites
- Responses and JSON API
- JWT API Authentication (Optional) for token-based SPAs
pip install flask-cors- Optional SPA client walkthrough: Vue Fetching API and CORS
Same-Origin vs Cross-Origin
| Setup | Origin | CORS needed? |
|---|---|---|
| Flask serves HTML + API on same host/port | Same | No |
| Vite dev server + Flask API | Different port | Yes |
Production SPA on CDN, API on api.example.com | Different subdomain | Yes |
Browser sends preflight OPTIONS for some cross-origin requests before POST with JSON.
Install Flask-CORS
pip install flask-corsGlobal config in factory:
Code explanation:
resourceslimits CORS to API paths—not every route needs open originsallow_headersmust includeAuthorizationfor JWT
Production:
origins = os.environ.get("CORS_ORIGINS", "").split(",")
CORS(app, resources={r"/api/*": {"origins": origins}})Never use origins: "*" with credentials: true.
Per-Route cross_origin
from flask_cors import cross_origin
@api_bp.route("/public/stats", methods=["GET"])
@cross_origin(origins="*")
def public_stats():
return {"visits": 1000}Use sparingly for truly public read-only endpoints.
Frontend fetch Example
Same pattern for Vite + Vue—see Vue Fetching API and CORS. Concept with React-style fetch:
const response = await fetch("http://127.0.0.1:5000/api/v1/posts", {
method: "GET",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
});
const data = await response.json();Dev proxy alternative—no CORS in dev:
vite.config.js:
export default {
server: {
proxy: {
"/api": "http://127.0.0.1:5000",
},
},
};Frontend calls /api/v1/posts on same origin; Vite forwards to Flask.
Tip
Proxy in Dev, CORS in Staging
Many teams proxy locally and configure real CORS only where origins differ in deployed environments.
API Version Prefix
app/api/__init__.py:
from flask import Blueprint
api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1")Register in factory:
from app.api.routes import api_v1_bp
app.register_blueprint(api_v1_bp)Versioning strategies:
| Strategy | Example |
|---|---|
| URL path | /api/v1/users |
| Header | Accept: application/vnd.myapp.v1+json |
| Query | /api/users?version=1 |
URL prefix is simplest for tutorials and REST project.
Unified JSON Errors
@api_v1_bp.errorhandler(400)
def bad_request(e):
return jsonify({"code": 400, "message": "bad request", "data": None}), 400SPAs parse consistent shapes—see Responses and JSON API.
Cookies vs JWT for SPAs
| Auth | CORS notes |
|---|---|
| JWT in Authorization header | Common for SPAs; set CORS allow_headers |
| Session cookie | Requires supports_credentials=True and explicit origins—no wildcard |
CORS(app, supports_credentials=True, origins=["https://app.example.com"])Frontend:
fetch(url, { credentials: "include" });OpenAPI Documentation (Concept)
Tools generate Swagger UI from Flask routes:
| Library | Notes |
|---|---|
| flask-smorest | Marshmallow schemas + OpenAPI 3 |
| flasgger | Docstrings to Swagger |
| APIFairy | Similar lightweight option |
Spring teams compare with SpringDoc on Spring Boot track—same OpenAPI standard, different integration.
Minimal flasgger idea:
def list_posts():
"""List posts
---
responses:
200:
description: OK
"""
return jsonify([])Not required for small projects—curl and pytest suffice early on.
Preflight Debugging
Browser Network tab shows:
OPTIONS /api/v1/posts → 200 with Access-Control-Allow-*
POST /api/v1/postsIf OPTIONS fails:
- Flask route missing
OPTIONSin methods (Flask-CORS usually handles) - Nginx blocking OPTIONS—allow in proxy config
FAQ
CORS error but Postman works?
CORS is browser-only—Postman ignores it; fix response headers for browsers.
Credentials not sent?
credentials: "include" on fetch and matching server supports_credentials.
Wildcard with Authorization?
Use explicit origin list when sending tokens.
Duplicate CORS headers?
Nginx and Flask both add headers—disable one layer.
WebSocket CORS?
Different mechanism—use flask-socketio CORS settings separately.
Mobile apps?
Native apps are not browsers—CORS does not apply; still use HTTPS and token auth.