Codeskill

Learn to code, step by step

JS front end + Python API

How a JavaScript front end talks to a Python API in practice: fetch from the browser, CORS, JSON contracts, and where to draw the line between server-rendered pages and a separate SPA. You met fetch in the JavaScript tutorials; here the Python side catches up.

Two common shapes

  • Flask serves HTML + static JS – same origin, cookies for auth, simple deployment
  • Static front end + JSON API – React/Vue/vanilla JS on one host, Flask on another or on /api – needs CORS and clear auth

Both are valid. The second pattern is what people mean by “front end + API”. This page focuses on that split.

Flask API with CORS

Browsers block cross-origin fetch unless the server sends the right CORS headers. Flask-CORS makes that configurable.

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": ["http://localhost:5173"]}})

ITEMS = [{"id": 1, "name": "Widget"}]

@app.get("/api/items")
def list_items():
    return jsonify(ITEMS)

In production, list real front-end origins – not * – when credentials are involved.

JavaScript client with fetch

Keep API base URLs in one config object. Handle non-OK responses explicitly.

// static/app.js (shown in a Python-labelled block for the export example)
// const API_BASE = "http://localhost:5000/api";
//
// async function loadItems() {
//   const response = await fetch(`${API_BASE}/items`);
//   if (!response.ok) {
//     const body = await response.json().catch(() => ({}));
//     throw new Error(body.error || response.statusText);
//   }
//   return response.json();
// }
//
// loadItems().then(items => console.log(items));

Real projects use the JavaScript tutorials’s module patterns. The important part is the contract: JSON shape, error format, and status codes match what the Python API returns.

Auth across origins

Cookie sessions work cross-origin only with careful SameSite and CORS credential settings. Many SPAs use a bearer token in an Authorization header instead – store it in memory, not localStorage, if you can help it (XSS steals localStorage).

from functools import wraps
from flask import request, abort

def require_token(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        auth = request.headers.get("Authorization", "")
        if not auth.startswith("Bearer ") or auth.split(" ", 1)[1] != "dev-token":
            abort(401)
        return func(*args, **kwargs)
    return wrapper

@app.post("/api/items")
@require_token
def create_item():
    ...

Shared types without TypeScript cosplay

Document JSON fields in OpenAPI (even a hand-written YAML) or share example payloads in both repos. You do not need generated clients on day one – you need agreement on field names and null handling.

Development workflow

Run Flask on port 5000 and a Vite or static dev server on 5173. Proxy API calls through the dev server to avoid CORS during local work, or enable Flask-CORS for localhost only. Pick one approach per project and document it in the README so the next developer does not chase phantom CORS errors for an hour.

When to skip a separate front end

Admin tools, internal dashboards, and small forms often ship faster as Flask templates with a sprinkling of JavaScript. Splitting into SPA + API adds deployment surface. Choose the split when interactivity, team boundaries, or mobile clients justify it – not because every tutorial on the internet does it that way.

PreviousSecurity for Python web apps