MAD Study Hub

Home / MAD2 / Week 5

Week 5 · Vue with APIs: fetch, axios and lifecycle

Official syllabus line: Vue with APIs
About 100 minQuiz 2 material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 week 4 (Vue instance, directives, lifecycle), MAD1 Flask REST APIs (status codes, JSON), and JavaScript promises at a basic level.

The one-minute story

Think of it like this

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 fetch and with axios, inside created or mounted, and with async/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/await code with success, HTTP error and network error.

Words you need

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. await pauses 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 OPTIONS request 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".)

The idea

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 a TypeError.
  • It does not reject for HTTP errors. A 404 or 500 is a successful round trip, so .then runs. You must check res.ok (true for status 200 to 299) or res.status.
  • res.json() rejects with a SyntaxError if the body is not valid JSON (for example HTML).
  • A res object has: ok, status, statusText, headers, and body readers json(), 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));
  • await can only be used inside an async function. It gives the resolved value, not the promise.
  • An async function always returns a promise. A throw inside it makes that promise reject.
  • If you forget await, res is a Promise: res.ok is undefined and res.json is not a function on the promise. A real question presents code like this and asks "what is the error".
Solved example: 1. Predict the console (four cases)
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 doesWhere it failsPrinted
network error (server unreachable)fetch rejects, outer catchResponse is not json
500 with a JSON bodyjson works, ok is false500
404 with a JSON bodysame404
200 with an HTML bodyjson() rejects, inner catchNetwork 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.

Solved example: 2. The classic network-vs-HTTP error check
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" }.

Watch out - real exam trap
  • fetch does not reject on 404 or 500. Always check res.ok.
  • res.json() is a promise: write await res.json() or another .then.
  • console.log(fetch(url)) prints Promise {<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 idea

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

Watch out - real exam trap

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);
}
fetchaxios
Installationbuilt into browsersnpm install axios
JSON bodyJSON.stringify + header yourselfpass an object, it does both
Read JSONawait res.json()res.data
HTTP 404 / 500resolves; check res.okrejects; use err.response.status
Network errorrejects (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/json tells Flask that the body is JSON, so request.get_json() works. Without it fetch labels a string body as text/plain, and recent Flask rejects it (415 Unsupported Media Type).
  • A real question asks which header is necessary: 'Content-Type': 'application/json' (not multipart/form-data, not application/text, and Accept: */* is not what the server needs).
  • Accept tells the server which type you want back; Authorization carries a token (see week 7). A fetch can add any custom header, send images (as FormData or Blob) and use any method.
  • For a file upload with FormData, do not set Content-Type yourself; the browser adds the boundary.

Status codes you should react to:

CodeMeaningWhat the Vue code does
200OKshow data
201Createdadd the new item to the list
204No Contentsuccess with empty body (DELETE): do not call res.json()
400Bad Requestshow the validation message
401Unauthorized (not logged in / bad token)send the user to the login page
403Forbidden (logged in, not allowed)show "not allowed"
404Not Foundshow "not found"
409Conflict (duplicate)show the message
500Server errorshow 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:

  1. Read: created runs loadTasks, filling tasks.
  2. Create: the form's @submit.prevent calls addTask; the body is JSON with a header; Flask answers 201 and the new task including its id; we push it (a reactive method).
  3. Update: PUT sends only the changed field. We copy the server's answer back (task.done already exists, so this is reactive).
  4. Delete: 204 has no body, so the helper returns null instead of calling res.json(); we remove it with filter.
  5. Errors from any call end in this.error, which the template shows.
Your turn: 1. Edit in place

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.

Your turn: 2. Which status codes

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.

Your turn: 3. Fix the bug

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.

In your project

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:

TargetSame origin?CORS error by default?
https://abc.comyesno
https://abc.com/pageyes (the path does not matter)no
https://xyz.abc.comno (different host: a subdomain is another origin)yes
https://abc8.comno (different host)yes
http://abc.comno (different scheme)yes
https://abc.com:8443no (different port)yes
The idea

How CORS works

  • It is enforced by the browser, not the server. A request from curl or 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/json body, or an Authorization header) the browser first sends a preflight OPTIONS request. The server must reply with Access-Control-Allow-Methods and Access-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.

Solved example: 3. Will the request succeed?
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

  1. 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.

  1. 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:

  1. In the Vue project run npm run build.
  2. Copy or point to dist/ from your Flask folder (or set the build output there).
  3. Use relative API URLs: fetch("/api/tasks") works in dev (through the proxy) and in production (same origin).
  4. If you use the Vue Router in history mode, add a Flask catch-all that returns index.html for unknown non-API paths, otherwise a page refresh on /about gives 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) and console.log(data) while learning. Use the Vue Devtools extension to see data change.

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

TechniqueIdea
Pollingclient asks again every few seconds, whether or not there is news
Long pollingclient asks once; the server holds the request open until data exists, then answers
WebSocketsone persistent two-way connection (chat)
Webhooksthe server calls your URL when an event happens
Pub/Subpublishers 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

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/catch inside try/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.com and https://abc8.com give errors, https://abc.com/page does 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/json header 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.
Watch out - real exam trap
  • fetch + 404 does not hit .catch.
  • await is not allowed outside async functions (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 204 has no body: res.json() fails.
  • Do not build SQL with f-strings from request data (SQL injection); use parameters or the ORM.

Quick quiz

Quick quiz

fetch("/api/missing") receives a 404 response with a JSON body. What happens?

  • the promise rejects and .catch runs
  • the promise resolves; res.ok is false and res.status is 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': '*/*'
The header tells the server the body is JSON.

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/page
  • https://xyz.abc.com
  • https://abc8.com
  • https://abc.com
A different host (including a subdomain) is another origin; a different path on the same origin is not.

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
Origin 5173 is not allowed, so Flask omits the allow header and the browser blocks the response.

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
An unthreaded server handles one request at a time: 20 + 30.

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.
The server declares, the browser enforces; tools that are not browsers ignore it.

Viva: say it out loud

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

Remember this
  • fetch(url, options) returns a promise of a Response. Call res.json() (also a promise). It rejects only on network or CORS failure; check res.ok for HTTP errors.
  • Use async/await in methods with try/catch/finally; keep loading, error and the data in data. Start with [] or {} (not null) or guard with v-if.
  • JSON POST/PUT: method, Content-Type: application/json, JSON.stringify body. 201 Created, 204 No Content (no body), 400, 401, 403, 404, 500.
  • CRUD in Vue: list in created, push after create, copy the response after update, filter after delete, one helper for status checking.
  • CORS: browser rule on origins (scheme + host + port); the server opts in with Access-Control-Allow-Origin; preflight OPTIONS for 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.
Code and facts to keep
  • 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 in err.response.status
  • Flask: CORS(app) or CORS(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="") and send_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 }
Connected weeks (learn them together)

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