Week 5 · Vue with APIs: fetch, axios and lifecycle
The one-minute story
A Vue page talking to Flask is a customer ordering at a restaurant. The Vue page is the customer at the table. The Flask API is the kitchen. The customer cannot walk into the kitchen: he writes an order slip (a request: what he wants, in what form), hands it to the waiter (fetch or axios), and waits. Meanwhile he does not freeze: he sips water (the page keeps working and shows "Loading...").
The kitchen sends back a dish and a note: "200 here it is" or "404 we do not have that" or "500 the chef set the stove on fire". A careful customer reads the note before he eats. Finally there is the building's rule about outsiders: a kitchen may refuse to serve people who came from another building. That is CORS, and the kitchen's manager (Flask-CORS) must put your building on the allowed list.
By the end of this week you can:
- call a Flask API from Vue with
fetchand withaxios, insidecreatedormounted, and withasync/await; - show a loading state and an error state, and handle a failed request correctly;
- build a full CRUD page (list, create, edit, delete) against a Flask REST backend;
- send JSON with the right header and read status codes;
- explain what CORS is, why the browser blocks a request, and enable it with Flask-CORS or avoid it with a dev proxy;
- predict the console output of
async/awaitcode with success, HTTP error and network error.
Words you need
- API - the URLs a server offers, which a program (our Vue page) can call instead of a person.
- Request / response - what the client sends, and what the server sends back (a status code, headers and a body).
- fetch - the browser's built-in function to make HTTP requests; it returns a Promise.
- Promise - a value that will arrive later; it ends as resolved (success) or rejected (failure).
- async / await - syntax to write promise code like normal top-to-bottom code.
awaitpauses only that function. - axios - a popular library (not built in) that wraps requests and turns JSON automatically.
- Status code - the number that says how it went: 200 OK, 201 Created, 204 No Content, 400 Bad Request, 401 Unauthorized, 404 Not Found, 500 Server Error.
- Origin - scheme + host + port, e.g.
http://localhost:8080. - CORS - Cross-Origin Resource Sharing: the way a server tells the browser which other origins may read its responses.
- Preflight - an automatic
OPTIONSrequest the browser sends before a "non-simple" request (JSON body, custom header, PUT/DELETE). - Proxy - the dev server forwards
/api/...requests to Flask, so the browser thinks everything is one origin. - Dev tools Network tab - the browser panel that lists every request with status, headers, payload and timing.
- CRUD - Create, Read, Update, Delete.
Idea 1: Calling an API with fetch
fetch(url, options) returns a Promise of a Response object. The response is not your data: you must ask for the body, which is another promise.
fetch("http://127.0.0.1:5000/api/tasks")
.then((res) => res.json()) // body -> JavaScript value (also a promise)
.then((data) => console.log(data)) // use the data
.catch((err) => console.error(err)); // network failure or a thrown error
console.log("End of script");
Output order: End of script first, then the data. fetch never blocks: JavaScript carries on with the next line and runs the .then code later, when the response arrives. (A real paper asks exactly this: the answer is "End of script, then the JSON".)
What fetch does and does not do
- It rejects (goes to
.catch) only when the request could not be completed at all: server down, no internet, DNS failure, or a CORS block. The error is aTypeError. - It does not reject for HTTP errors. A
404or500is a successful round trip, so.thenruns. You must checkres.ok(true for status 200 to 299) orres.status. res.json()rejects with aSyntaxErrorif the body is not valid JSON (for example HTML).- A
resobject has:ok,status,statusText,headers, and body readersjson(),text(),blob().
The same with async and await
async function getTasks() {
const res = await fetch("http://127.0.0.1:5000/api/tasks");
if (!res.ok) throw new Error("HTTP " + res.status);
return await res.json();
}
getTasks().then((d) => console.log(d)).catch((e) => console.log(e.message));
awaitcan only be used inside anasyncfunction. It gives the resolved value, not the promise.- An
asyncfunction always returns a promise. Athrowinside it makes that promise reject. - If you forget
await,resis a Promise:res.okisundefinedandres.jsonis not a function on the promise. A real question presents code like this and asks "what is the error".
async function getResponse(url) {
try {
const response = await fetch(url);
try {
const data = await response.json();
if (response.ok) { return data; } else { return response.status; }
} catch {
return "Network Error";
}
} catch {
return "Response is not json";
}
}
getResponse("url").then((data) => console.log(data));
The labels are mixed up on purpose; read the structure, not the words. The outer try wraps only the fetch call; the inner try wraps response.json(). I ran this against a small test server:
| What the URL does | Where it fails | Printed |
|---|---|---|
| network error (server unreachable) | fetch rejects, outer catch | Response is not json |
| 500 with a JSON body | json works, ok is false | 500 |
| 404 with a JSON body | same | 404 |
| 200 with an HTML body | json() rejects, inner catch | Network Error |
Second variant (also a real paper): the same try inside try, but an if (!res.ok) throw new Error(...) sits inside the outer try and the outer catch always throws new Error("Data is not JSON serializable"). With a URL that returns valid HTML, res.ok is true, json() fails, the inner catch throws Error("Error"), and the outer catch catches that and throws "Data is not JSON serializable". With a 404 you get the same message, because the thrown HTTP error is also caught by the outer catch. The lesson: trace which try each statement is inside.
async function fetchData() {
try {
const response = await fetch("https://api.example.com/data");
if (!response.ok) throw new Error("HTTP error");
return await response.json();
} catch (error) {
if (error.name === "TypeError") return { status: "network error" };
return { status: "http error" };
}
}
If the server is unreachable, fetch rejects with a TypeError, so the function returns { status: "network error" }. If the server answers 500, the thrown Error has name Error, so you get { status: "http error" }.
fetchdoes not reject on 404 or 500. Always checkres.ok.res.json()is a promise: writeawait res.json()or another.then.console.log(fetch(url))printsPromise {<pending>}, not data.- Code after a
fetch(...)call runs before the response arrives (the "End of script comes first" question).
Idea 2: Calling the API from a Vue component
Where to start the request? In created (data and methods exist, request starts early) or mounted (the page exists). Both are fine for plain API calls; the course examples use either. Then store the answer in data and let Vue re-render.
<div id="app">
<p v-if="loading">Loading...</p>
<p v-else-if="error" class="err">{{ error }}</p>
<ul v-else>
<li v-for="t in tasks" :key="t.id">{{ t.title }}</li>
</ul>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({
el: "#app",
data: { tasks: [], loading: false, error: "" },
created() { this.loadTasks(); },
methods: {
async loadTasks() {
this.loading = true;
this.error = "";
try {
const res = await fetch("http://127.0.0.1:5000/api/tasks");
if (!res.ok) throw new Error("HTTP " + res.status);
this.tasks = await res.json();
} catch (e) {
this.error = "Could not load tasks: " + e.message;
} finally {
this.loading = false;
}
},
},
});
</script>
Order of events (verified by running a similar component): the page renders at once with tasks: [] and loading: false; created calls loadTasks, which sets loading = true and returns at the first await; Vue mounts (so mounted runs before the data arrives); when the response comes, tasks is filled, finally sets loading = false, and Vue re-renders the list. finally runs on both success and failure, so the "Loading..." message never gets stuck.
The three states of every request: loading, error, data. Your data should hold loading, error and the result. Show them with v-if / v-else-if / v-else. Start with an empty but valid value ([] for lists, {} or null for one record).
Initial value of a single record. With data() { return { user: {} } } and the template Hi {{ user.name }}, the page shows Hi (empty) until the data arrive, with no error. With user: null the same template throws "Cannot read properties of null" on first render (I ran both). Fix: use {}, or guard with v-if="user".
A real question shows exactly this: user: {}, created() calling an async fetchUserData that reads this.$route.query.userId and fetches /api/users/123. Answer: the component displays the name correctly once the response arrives. The first render has no name, which is fine.
Also: a slow API call does not block the page. Everything else (a sidebar in a parent component, for example) renders immediately; only the part that needs the data fills in later.
The same with axios
axios is an installed package (npm install axios) or a CDN script. Compared with fetch:
import axios from "axios"; // with the CDN script it is the global `axios`
const res = await axios.get("http://127.0.0.1:5000/api/tasks");
console.log(res.data); // already parsed JSON, no .json() step
const created = await axios.post("http://127.0.0.1:5000/api/tasks", { title: "Buy milk" });
// the object is sent as JSON with Content-Type: application/json automatically
console.log(created.status, created.data);
try {
await axios.get("http://127.0.0.1:5000/api/tasks/999");
} catch (err) {
if (err.response) console.log(err.response.status); // server answered 4xx/5xx
else console.log("network or CORS problem", err.message);
}
fetch | axios | |
|---|---|---|
| Installation | built into browsers | npm install axios |
| JSON body | JSON.stringify + header yourself | pass an object, it does both |
| Read JSON | await res.json() | res.data |
| HTTP 404 / 500 | resolves; check res.ok | rejects; use err.response.status |
| Network error | rejects (TypeError) | rejects; err.response is undefined |
Idea 3: Sending data: JSON, headers and status codes
A POST that sends JSON needs three things: the method, the header and the stringified body.
const data = { name: "Aditya", score: 95 };
fetch("/api/submit", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
Content-Type: application/jsontells Flask that the body is JSON, sorequest.get_json()works. Without itfetchlabels a string body astext/plain, and recent Flask rejects it (415 Unsupported Media Type).- A real question asks which header is necessary:
'Content-Type': 'application/json'(notmultipart/form-data, notapplication/text, andAccept: */*is not what the server needs). Accepttells the server which type you want back;Authorizationcarries a token (see week 7). Afetchcan add any custom header, send images (asFormDataorBlob) and use any method.- For a file upload with
FormData, do not setContent-Typeyourself; the browser adds the boundary.
Status codes you should react to:
| Code | Meaning | What the Vue code does |
|---|---|---|
| 200 | OK | show data |
| 201 | Created | add the new item to the list |
| 204 | No Content | success with empty body (DELETE): do not call res.json() |
| 400 | Bad Request | show the validation message |
| 401 | Unauthorized (not logged in / bad token) | send the user to the login page |
| 403 | Forbidden (logged in, not allowed) | show "not allowed" |
| 404 | Not Found | show "not found" |
| 409 | Conflict (duplicate) | show the message |
| 500 | Server error | show a general error |
A real question: a fetch("http://127.0.0.1:5000/protected", { method: "GET" }) with no token to a protected route. Answer: the request fails with 401 Unauthorized (fetch itself still resolves; the response has status 401).
Idea 4: A full CRUD page against Flask
Backend (Flask 3, plain routes, in-memory list so it is easy to read). I tested every route with Flask's test client: create returns 201, a missing title returns 400, an unknown id returns 404 and delete returns 204.
from flask import Flask, jsonify, request
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/api/*": {"origins": "http://localhost:8080"}})
tasks = [{"id": 1, "title": "Learn Vue", "done": False}]
next_id = 2
@app.get("/api/tasks")
def list_tasks():
return jsonify(tasks)
@app.post("/api/tasks")
def create_task():
global next_id
data = request.get_json(silent=True)
if not data or not data.get("title"):
return jsonify({"error": "title is required"}), 400
task = {"id": next_id, "title": data["title"], "done": False}
next_id += 1
tasks.append(task)
return jsonify(task), 201
@app.put("/api/tasks/<int:task_id>")
def update_task(task_id):
task = next((t for t in tasks if t["id"] == task_id), None)
if task is None:
return jsonify({"error": "not found"}), 404
data = request.get_json(silent=True) or {}
task["title"] = data.get("title", task["title"])
task["done"] = data.get("done", task["done"])
return jsonify(task)
@app.delete("/api/tasks/<int:task_id>")
def delete_task(task_id):
global tasks
before = len(tasks)
tasks = [t for t in tasks if t["id"] != task_id]
if len(tasks) == before:
return jsonify({"error": "not found"}), 404
return "", 204
Frontend (Vue 2 from a CDN). One helper request handles the checks for every call, so each method stays short. I ran this code against a fake server in a browser-like environment and checked list, add, empty-title error, toggle, delete and network failure.
<div id="app">
<p v-if="loading">Loading...</p>
<p v-if="error" class="err">{{ error }}</p>
<form @submit.prevent="addTask">
<input v-model.trim="title" placeholder="New task">
<button :disabled="!title">Add</button>
</form>
<ul>
<li v-for="t in tasks" :key="t.id">
<span :class="{ done: t.done }" @click="toggle(t)">{{ t.title }}</span>
<button @click="removeTask(t)">Delete</button>
</li>
</ul>
</div>
const API = "http://127.0.0.1:5000/api/tasks";
new Vue({
el: "#app",
data: { tasks: [], title: "", loading: false, error: "" },
created() { this.loadTasks(); },
methods: {
async request(url, options) {
this.error = "";
const res = await fetch(url, options);
if (!res.ok) {
let msg = "HTTP " + res.status;
try { msg = (await res.json()).error || msg; } catch (e) {}
throw new Error(msg);
}
return res.status === 204 ? null : res.json();
},
async loadTasks() {
this.loading = true;
try { this.tasks = await this.request(API); }
catch (e) { this.error = e.message; }
finally { this.loading = false; }
},
async addTask() { // Create
try {
const task = await this.request(API, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: this.title }),
});
this.tasks.push(task);
this.title = "";
} catch (e) { this.error = e.message; }
},
async toggle(task) { // Update
try {
const updated = await this.request(API + "/" + task.id, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ done: !task.done }),
});
task.done = updated.done;
} catch (e) { this.error = e.message; }
},
async removeTask(task) { // Delete
try {
await this.request(API + "/" + task.id, { method: "DELETE" });
this.tasks = this.tasks.filter((t) => t.id !== task.id);
} catch (e) { this.error = e.message; }
},
},
});
How the parts fit:
- Read:
createdrunsloadTasks, fillingtasks. - Create: the form's
@submit.preventcallsaddTask; the body is JSON with a header; Flask answers201and the new task including its id; wepushit (a reactive method). - Update:
PUTsends only the changed field. We copy the server's answer back (task.donealready exists, so this is reactive). - Delete:
204has no body, so the helper returnsnullinstead of callingres.json(); we remove it withfilter. - Errors from any call end in
this.error, which the template shows.
Add an "Edit" feature: clicking Edit turns the title into an input; pressing Enter sends a PUT with the new title. What data do you add and which request?
Need a hint?
You need to remember which task is being edited and its draft text.
Show the full solution
Add editingId: null and draft: "" to data. The Edit button sets editingId = t.id; draft = t.title. The template shows v-if="editingId === t.id" an input v-model="draft" @keyup.enter="saveEdit(t)". saveEdit does PUT API/id with body { title: this.draft }, then t.title = updated.title; this.editingId = null.
For the Flask API above, what status does each give? (a) POST with {}; (b) DELETE of id 999; (c) POST with {"title": "x"}; (d) DELETE of an existing id.
Need a hint?
Look at each return line.
Show the full solution
(a) 400, (b) 404, (c) 201, (d) 204 with an empty body.
A student writes async loadTasks() { const res = fetch(API); this.tasks = res.json(); } and the list stays empty with an error in the console. List the problems.
Need a hint?
What type is fetch(...) without await?
Show the full solution
fetch(API) returns a Promise, so res is a Promise and res.json does not exist on it. Write const res = await fetch(API), then this.tasks = await res.json(). Also add if (!res.ok) throw ... and a try/catch/finally for the error and loading states.
When the examiner says "show me an API failing", stop Flask and reload the page. Your UI must show a clear error message (not a blank page or a stuck "Loading..."). Also show a 400 from a bad form. Be ready to explain where you put the try/catch/finally and why loading goes back to false.
Idea 5: CORS: why the browser blocks and how to fix it
The browser has a safety rule, the same-origin policy: JavaScript from one origin may not read responses from another origin, unless that server says it is allowed. An origin is scheme + host + port; all three must match.
Your Vue dev page is at http://localhost:8080. Flask is at http://127.0.0.1:5000. Those are different origins (different host and port), so a fetch from the page to Flask is a cross-origin request, and without CORS headers the browser blocks the response and prints a CORS error in the console.
Origin comparison (a recurring exam question), page loaded from https://abc.com:
| Target | Same origin? | CORS error by default? |
|---|---|---|
https://abc.com | yes | no |
https://abc.com/page | yes (the path does not matter) | no |
https://xyz.abc.com | no (different host: a subdomain is another origin) | yes |
https://abc8.com | no (different host) | yes |
http://abc.com | no (different scheme) | yes |
https://abc.com:8443 | no (different port) | yes |
How CORS works
- It is enforced by the browser, not the server. A request from
curlor Postman is never blocked. - The server opts in by sending a header:
Access-Control-Allow-Origin: http://localhost:8080(or*for any origin). CORS lets the server say which origins may load its resources; it is not the client that chooses. - Same-origin requests need no CORS.
- For "non-simple" requests (methods PUT/DELETE, a
Content-Type: application/jsonbody, or anAuthorizationheader) the browser first sends a preflightOPTIONSrequest. The server must reply withAccess-Control-Allow-MethodsandAccess-Control-Allow-Headers; only then is the real request sent. - A blocked request often still reaches the server and runs; the browser just refuses to give your JavaScript the answer. That is why CORS is not a protection against someone who calls your API directly.
Enabling CORS in Flask
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
CORS(app) # allow every origin (fine for local learning)
# or restrict:
# CORS(app, resources={r"/api/*": {"origins": "http://localhost:8080"}})
I tested the restricted version: a request with Origin: http://localhost:8080 gets Access-Control-Allow-Origin: http://localhost:8080; a request with Origin: http://localhost:5173 gets no such header (status 200, but the browser blocks it); a preflight OPTIONS gets the allowed methods and headers.
CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}})
@app.route("/api/data")
def data():
return {"msg": "Success"}
fetch("http://localhost:5000/api/data").then(r => r.json()).then(console.log);
The Vue app runs at http://localhost:5173. Only http://localhost:3000 is allowed, so Flask sends no allow header for origin 5173. The browser shows a CORS error (it is not a 404 and the server does not crash; the server did answer). This is a real paper question.
A real alternative sets the header by hand: return "Endpoint 1 Accessed", 200, {"Access-Control-Allow-Origin": "*"}. That is the same thing without the extension.
Other ways to avoid CORS
- Dev proxy. The Vue dev server forwards some paths to Flask, so the page calls its own origin:
// vue.config.js (Vue CLI)
module.exports = {
devServer: {
proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } },
},
};
Now the page at http://localhost:8080 calls fetch("/api/tasks") (a relative URL). The dev server passes it to Flask. The browser sees one origin, so no CORS. (With Vite the same idea is server.proxy in vite.config.js.) The proxy is for development only.
- Serve both from one origin (next idea). In production the built Vue files and the API come from Flask, so there is no cross-origin call.
Idea 6: Putting a built Vue app inside Flask (outline)
npm run build (Vue CLI) produces a dist/ folder: index.html, plus js/ and css/ with minified files. These are plain static files. Flask can serve them:
from flask import Flask, send_from_directory
app = Flask(__name__, static_folder="dist", static_url_path="")
@app.get("/")
def index():
return send_from_directory(app.static_folder, "index.html")
# the API lives under /api/..., so the same origin serves page and data
I checked that / returns the built index.html. Steps in practice:
- In the Vue project run
npm run build. - Copy or point to
dist/from your Flask folder (or set the build output there). - Use relative API URLs:
fetch("/api/tasks")works in dev (through the proxy) and in production (same origin). - If you use the Vue Router in
historymode, add a Flask catch-all that returnsindex.htmlfor unknown non-API paths, otherwise a page refresh on/aboutgives Flask's 404 (week 6).
Idea 7: The response lifecycle and debugging
What happens when a user clicks "Load":
click (@click) -> method runs -> loading = true -> fetch() sends the request
| |
Vue re-renders "Loading..." Flask route runs, returns JSON + status
| |
response arrives (browser checks CORS) -> res.ok? -> res.json() -> this.tasks = data
|
finally: loading = false -> Vue re-renders the list
Debugging with the browser Network tab (F12, then Network):
- Each request is a row: name, status, type, size, time. Red means failure.
- Click a row: Headers (request URL, method, status, request and response headers, including
Access-Control-Allow-Origin), Payload (what you sent), Preview / Response (what came back), Timing. - A CORS failure shows a message in the Console ("blocked by CORS policy"), and the row shows a CORS error or no response.
- Typical diagnosis: status 404 = wrong URL; 405 = wrong method; 400 = bad body or missing
Content-Type; 415 = Flask did not get JSON; 500 = look at the Flask terminal; no row at all = the code never ran or Flask is not running. - Add
console.log(res.status)andconsole.log(data)while learning. Use the Vue Devtools extension to seedatachange.
Timing questions (very common in the papers)
@app.route("/endpoint1")
def method1():
time.sleep(20)
return "Endpoint 1 Accessed", 200, {"Access-Control-Allow-Origin": "*"}
@app.route("/endpoint2")
def method2():
time.sleep(30)
return "Endpoint 2 Accessed", 200, {"Access-Control-Allow-Origin": "*"}
app.run(threaded=False)
A page issues fetch to both URLs one after the other. The browser sends both at once, but a server started with threaded=False handles one request at a time, so total time until both are done is 20 + 30 = 50 seconds. With a threaded server (the default for flask run and app.run() in Flask 1.0 and later) they run in parallel and take about the larger one, 30 seconds. A variant: two routes sleeping 10 and 30 seconds with app.run(debug=True) (threaded), the page opened at 10:10:10, each route returning datetime.now().second. The logs show 20 (10 + 10) and 40 (10 + 30). The page itself loads at once in all cases (answer "0 seconds" for "when does the static page show"), because fetch does not block rendering.
Other ways to get fresh data
| Technique | Idea |
|---|---|
| Polling | client asks again every few seconds, whether or not there is news |
| Long polling | client asks once; the server holds the request open until data exists, then answers |
| WebSockets | one persistent two-way connection (chat) |
| Webhooks | the server calls your URL when an event happens |
| Pub/Sub | publishers send messages to a topic; many subscribers receive them |
Match used in the papers: webhooks = notification when an event occurs; websockets = real-time chat; API = request/response over HTTP; polling = fetch periodically; pub/sub = broadcast to many subscribers.
Exam radar
The evidence file counts questions about this week's topics as about 45 of 608 Quiz 2 and 90 of 1471 End Term questions (keyword match, so approximate; none from Quiz 1), and about 36 distinct questions were read. They cluster in these shapes:
- Predict the output of fetch/async code with
try/catchinsidetry/catch(success, HTTP error code, HTML instead of JSON, network failure). Answers hinge on which block catches what. - CORS facts and origin comparisons (
https://xyz.abc.comandhttps://abc8.comgive errors,https://abc.com/pagedoes not; same-origin needs no CORS; CORS lets the server say which origins may load resources) and Flask-CORS with a restricted origin. - Timing with
threaded=False(50 seconds) or threaded (parallel), and "End of script" before the data. - Required
Content-Type: application/jsonheader for a JSON POST. - Created hook with an async fetch and what the page shows (week 4 and here).
- Security reading questions: string-built SQL in Flask (SQL injection), no authentication check on the route, plain-text passwords, no token mechanism. The usual answer is "all of these".
- Concept matching: webhooks, websockets, polling, long polling, pub/sub; REST versus GraphQL (REST responses can be cached; GraphQL works with any language that can make HTTP requests; it is not "always better").
- Page speed: factors are the number of HTTP requests and the file sizes.
fetch+ 404 does not hit.catch.awaitis not allowed outsideasyncfunctions (in a plain method it is a syntax error).- "Which origin gives a CORS error": compare scheme, host and port; paths do not matter; subdomains do.
- CORS is enforced by the browser; server-to-server calls and Postman ignore it.
- A DELETE returning
204has no body:res.json()fails. - Do not build SQL with f-strings from request data (SQL injection); use parameters or the ORM.
Quick quiz
fetch("/api/missing") receives a 404 response with a JSON body. What happens?
- the promise rejects and
.catchruns - the promise resolves;
res.okis false andres.statusis 404 - fetch throws a SyntaxError
- the response is silently dropped
fetch rejects only when the request cannot be completed; HTTP error codes are normal responses.An async function does const res = await fetch(url); const data = await res.json(); in a try, and the catch returns "failed". The URL returns HTTP 200 with an HTML page. What is returned?
- the HTML text
- the string "200"
- "failed"
- undefined
res.json() rejects because HTML is not valid JSON, so control goes to catch.Which header is necessary for a fetch POST that sends JSON.stringify(data) as the body to a Flask route reading request.get_json()?
'Content-Type': 'multipart/form-data''Content-Type': 'application/text''Content-Type': 'application/json''Accept': '*/*'
A page at https://abc.com calls fetch. Select the targets that cause a CORS error by default. (select all that apply)
https://abc.com/pagehttps://xyz.abc.comhttps://abc8.comhttps://abc.com
Flask has CORS(app, resources={r"/api/*": {"origins": "http://localhost:3000"}}). The Vue app at http://localhost:5173 calls http://localhost:5000/api/data. What happens?
- the request succeeds
- a CORS error occurs in the browser
- the server crashes
- a 404 is returned
A page fires two fetch calls at once to Flask routes that sleep 20 and 30 seconds. Flask runs with threaded=False. About how long until both responses are logged?
- 20 seconds
- 30 seconds
- 10 seconds
- 50 seconds
Select all TRUE statements about CORS. (select all that apply)
- It lets a server say which origins may load its resources.
- Same-origin requests do not need CORS.
- The client page decides which servers it may call, by setting a header.
- It is enforced by Flask, so Postman is blocked too.
Viva: say it out loud
How does your Vue app get data from the Flask backend?
In created I call a method that uses fetch (or axios) to send a request to the Flask route, check res.ok, parse the JSON, and store it in data. Vue then re-renders the list. The method is async, uses try/catch/finally, and sets a loading flag around the call.
What is the difference between fetch and axios?
fetch is built into the browser, resolves even on HTTP 404 or 500 so I must check res.ok, and needs res.json(). axios is a library that parses JSON for me (res.data), sends objects as JSON automatically, and rejects for non-2xx status codes, with the status in err.response.status.
Show me how you send a JSON POST. Why that header?
I use method: "POST", headers: { "Content-Type": "application/json" } and body: JSON.stringify(obj). The header tells Flask the body is JSON so request.get_json() can parse it; without it Flask may answer 415 or give me None.
What is CORS and how did you solve it?
Browsers block a page from reading responses of another origin (different scheme, host or port) unless the server allows it. My Vue dev server is on port 8080 and Flask on 5000, so I used Flask-CORS to allow my origin. In development I can also use a dev-server proxy, and in production Flask serves the built files from the same origin.
The page shows "Loading..." forever. What do you check?
I open the Network tab to see whether the request was sent and what status came back. Then I check that loading is reset in a finally block, so it goes back to false on both success and error. A CORS block or a stopped Flask server also lands in the catch, which should show an error message.
How do you tell a network error from an HTTP error in fetch?
A network or CORS failure makes fetch reject with a TypeError, so it goes to catch. An HTTP error such as 404 resolves normally, so I check res.ok or res.status and throw my own error. With axios, err.response exists for HTTP errors and is missing for network errors.
How do you deploy the Vue frontend with Flask?
I run npm run build, which produces a dist folder of static files. Flask serves it with static_folder="dist" and a route for / that returns index.html. My API stays under /api, so everything is one origin and I use relative URLs.
Recap
fetch(url, options)returns a promise of a Response. Callres.json()(also a promise). It rejects only on network or CORS failure; checkres.okfor HTTP errors.- Use
async/awaitin methods withtry/catch/finally; keeploading,errorand the data indata. Start with[]or{}(notnull) or guard withv-if. - JSON POST/PUT: method,
Content-Type: application/json,JSON.stringifybody. 201 Created, 204 No Content (no body), 400, 401, 403, 404, 500. - CRUD in Vue: list in
created,pushafter create, copy the response after update,filterafter delete, one helper for status checking. - CORS: browser rule on origins (scheme + host + port); the server opts in with
Access-Control-Allow-Origin; preflightOPTIONSfor JSON/PUT/DELETE/Authorization; fix with Flask-CORS, a dev proxy or same-origin serving. - A non-threaded Flask server runs requests one by one (20 + 30 = 50 s); async code lets "End of script" print first.
- Debug in the Network tab: status, request payload, response, headers, timing.
const res = await fetch(url); if (!res.ok) throw new Error(res.status); const data = await res.json();fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) })- axios:
const { data } = await axios.get(url),axios.post(url, obj), errors inerr.response.status - Flask:
CORS(app)orCORS(app, resources={r"/api/*": {"origins": "http://localhost:8080"}}) - Return codes:
return jsonify(obj), 201,return "", 204,return jsonify({"error": "..."}), 404 - Dev proxy (
vue.config.js):devServer: { proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } } } - Serve build:
Flask(__name__, static_folder="dist", static_url_path="")andsend_from_directory(app.static_folder, "index.html") - Origin = scheme + host + port; path does not matter; subdomain differs
- Loading pattern:
loading = true; try { ... } catch (e) { error = e.message } finally { loading = false }
- MAD1 week 5 - Controllers: Flask routes and business logic: Every Flask route you wrote becomes an API call that your Vue code makes with fetch or axios.
- MAD1 week 6 - APIs and REST: OpenAPI and Flask-RESTful: The REST API you built in MAD1 week 6 is exactly what Vue consumes in MAD2 week 5.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All Quiz 2 papers