Home / Viva prep / MAD2 questions
282 questions with model answers, grouped by topic. Answer out loud first, then open the answer. Tick the ones you can answer; the count is saved in your browser.
var is function-scoped and hoisted with the value undefined. let and const are block-scoped and live in the temporal dead zone until their declaration line runs, so using them earlier throws a ReferenceError. const stops you from re-assigning the variable, but an object or array held in a const can still be mutated. In my code I use const by default and let only when the value must change.
== compares after type coercion, so 0 == "0" and null == undefined are true. === compares both type and value with no coercion, so 0 === "0" is false. Strict equality is predictable, so I use === everywhere. The only common use of == is x == null to catch both null and undefined.
A closure is a function that keeps access to the variables of the scope where it was created, even after that outer function has returned. The variables stay alive because the inner function still references them.
function counter() {
let n = 0;
return () => ++n;
}
const next = counter();
next(); // 1
next(); // 2
Here n is private to counter and only next can change it. Closures are what make callbacks, event handlers and module-style privacy work.
var i is a single function-scoped variable shared by all three callbacks. The callbacks run after the loop ends, when i is already 3. Changing var to let creates a fresh binding of i for each iteration, so the closures capture 0, 1 and 2. The older fix is wrapping the body in an IIFE that receives i as a parameter.
Before running the code, JavaScript registers declarations in their scope. Function declarations are hoisted completely, so you can call them before they appear. var variables are hoisted and initialised to undefined. let and const are hoisted too, but stay uninitialised in the temporal dead zone, so reading them early throws an error. Function expressions and arrow functions assigned to a variable follow that variable's rule.
An arrow function has no this, arguments or super of its own; it uses the this of the surrounding scope (lexical this). It also cannot be used as a constructor with new. That is why arrows are good for callbacks inside methods, while object methods and Vue 2 methods should be normal functions so that this points to the instance.
The arrow function takes this from the enclosing module scope, not from the Vue instance, so this.count is undefined or throws an error. Vue binds methods to the instance, but it cannot rebind an arrow. Write save() { this.count++; } or save: function () { ... }. Arrows are fine for callbacks nested inside a method, because they inherit the method's this.
this is decided by how a function is called, not where it is written. In a method call obj.f() it is obj; with new it is the new object; with call, apply or bind it is whatever you pass; in a plain call it is undefined in strict mode (or the global object otherwise). Arrow functions do not have their own this and use the surrounding one.
Every object has an internal link to another object called its prototype. When you read a property that the object does not have, JavaScript walks up this prototype chain until it finds the property or reaches null. Functions used with new set the new object's prototype to Fn.prototype, and class syntax is mostly a cleaner way to write the same mechanism. Methods put on the prototype are shared by all instances instead of copied.
An execution context is the environment in which code runs: it holds the variables, the scope chain and the value of this. There is a global context, and each function call creates a new one, which is pushed on the call stack and popped when the function returns. JavaScript has a single call stack, so it runs one thing at a time. Asynchronous callbacks only run when the stack is empty.
map returns a new array with each element transformed. filter returns a new array with only the elements for which the callback returns true. forEach just runs a function for each element and returns undefined. reduce folds the array into a single value using an accumulator.
const total = [1, 2, 3].reduce((acc, x) => acc + x, 0); // 6Destructuring unpacks values from arrays or objects into variables: const { id, name } = user; or const [a, b] = arr;. Spread ... expands an iterable or object in place, which is handy for copying and merging: const next = { ...state, loading: false }; or [...list, item]. I use spread to produce new objects instead of mutating, for example inside Vuex mutations or when copying a prop to local data.
ES modules let each file keep its own scope and share code with export and import. In a plain browser page you need <script type="module"> and real file paths. In a Vue CLI project, webpack bundles the modules, resolves npm packages, compiles .vue files and minifies the output for production. Modules avoid global variable clashes and make dependencies explicit.
A shallow copy such as {...obj} or arr.slice() copies only the top level, so nested objects are still shared between the original and the copy. A deep copy duplicates nested structures as well; structuredClone(obj) works for plain data, and JSON.parse(JSON.stringify(obj)) works for JSON-safe data but loses functions, dates and undefined. This matters in Vue and Vuex when you copy state and then edit it: a shallow copy can silently mutate the original.
In a condition JavaScript converts a value to a boolean. The falsy values are false, 0, -0, 0n, "", null, undefined and NaN; everything else is truthy, including "0", [] and {}. This is why v-if="items" is true for an empty array, and you should write items.length when you mean non-empty.
typeof null === "object" is a historical bug in the language that was never fixed for compatibility, so test null with x === null. NaN is the only value not equal to itself, so NaN === NaN is false; use Number.isNaN(x). Arrays also report "object", so use Array.isArray(x).
JSON.stringify(obj) turns a JavaScript value into a JSON string, and JSON.parse(str) turns a JSON string back into a value. We use them to send a body with fetch (body: JSON.stringify(data)) and to keep objects in localStorage, which only stores strings. JSON.parse throws on invalid input, so wrap it in try/catch when the data is not trusted.
When an element is clicked, the event first travels down in a capture phase and then bubbles up from the target through its ancestors, so listeners on the td, then tr, then table, then body fire in that order (for the default bubbling listeners). event.stopPropagation() stops it going further. Event delegation means attaching one listener to a parent and checking event.target, which works well for long or dynamic lists. Capturing listeners (third argument true) run on the way down, before the bubbling ones.
All three set this explicitly. fn.call(obj, a, b) runs the function immediately with arguments listed, fn.apply(obj, [a, b]) does the same with an array of arguments, and fn.bind(obj) returns a new function with this fixed for later. bind is useful when passing a method as a callback, such as setTimeout(this.save.bind(this), 100).
o.hi() is a method call so this is o and it prints "A". f() is a plain call: this is undefined in strict mode (which throws if you access .name) or the global object in sloppy mode (usually undefined/empty name in a browser). Detaching a method loses its this. Fix with o.hi.bind(o).
JavaScript runs on one thread with a call stack. Asynchronous work, such as timers, fetch and DOM events, is handled by the browser, and when it finishes a callback is placed in a queue. The event loop takes the next callback from the queue only when the call stack is empty. Promise callbacks go into the microtask queue, which is fully drained before the next macrotask such as a setTimeout callback.
The output is A, D, C, B. A and D are synchronous. The promise callback is a microtask and runs right after the current script finishes. The timer callback is a macrotask and runs later, even with a delay of 0.
Nested callbacks lead to deeply indented "callback hell" with scattered error handling. A promise represents a future result and can be chained with .then(), with one .catch() for any failure in the chain. It has three states: pending, fulfilled and rejected, and once settled it never changes. async/await is syntax that lets you consume promises in a sequential style.
An async function always returns a Promise; a returned value is wrapped in a resolved promise and a thrown error becomes a rejection. await pauses only that function until the awaited promise settles, then gives you the value or throws the rejection. The rest of the program is not blocked, because the function is suspended and the event loop continues.
Wrap the awaited calls in try/catch (optionally with finally to reset a loading flag), or attach .catch() to the returned promise.
async function load() {
try {
const res = await fetch("/api/lots");
if (!res.ok) throw new Error("HTTP " + res.status);
return await res.json();
} catch (e) {
console.error(e);
} finally {
// hide spinner
}
}No. fetch rejects only on network-level failure such as no connection or a CORS block. A 404 or 500 still resolves with a Response object, so you must check res.ok or res.status yourself. Axios differs here: it rejects for any status outside 2xx by default.
const res = await fetch("/api/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
res.json() itself returns a promise, so it needs await or .then. Without the Content-Type header, Flask's request.get_json() will not parse the body as JSON by default.
Promise.all resolves with an array of all results, but rejects as soon as any one promise rejects. Promise.allSettled waits for all and returns each outcome as fulfilled or rejected, never rejecting. Promise.race settles with the first promise to settle, whether success or failure. Promise.any resolves with the first promise that fulfils and only rejects if all of them reject.
Awaiting one by one runs them sequentially, so total time is the sum of all three. Starting them together and awaiting with Promise.all runs them in parallel, so total time is about the slowest one.
const [lots, users, stats] = await Promise.all([
fetch("/api/lots").then(r => r.json()),
fetch("/api/users").then(r => r.json()),
fetch("/api/stats").then(r => r.json()),
]);
Only do sequential awaits when a call depends on the result of the previous one.
forEach ignores the promises returned by the callback, so it returns immediately and the code after it runs before the saves complete. To run them one after another use for (const x of items) { await save(x); }. To run them in parallel and wait, use await Promise.all(items.map(x => save(x))).
Promise.all finishes when the slowest promise finishes, so for tasks of 3 s and 2 s started together it takes about 3 s. Awaiting them one after another takes about 5 s (3 + 2). The solo 4 s task would beat the sequential team but lose to the parallel team. This shows that parallel promises take the maximum time, sequential ones take the sum.
Debouncing delays running a function until the user has stopped triggering it for a fixed time, for example 300 ms after the last keystroke in a search box. It avoids sending an API request for every key press. In Vue you can implement it with setTimeout and clearTimeout inside a watcher or method.
let t;
function onInput(q) {
clearTimeout(t);
t = setTimeout(() => search(q), 300);
}It schedules fn as a macrotask after at least the given delay, so it runs only after the current code and all pending microtasks (promise callbacks) have finished. The delay is a minimum, not a guarantee, because the call stack must be empty first.
Create an AbortController, pass its signal to fetch, and call controller.abort() to cancel; the fetch promise rejects with an AbortError, which you should ignore.
const c = new AbortController();
fetch("/api/search?q=abc", { signal: c.signal });
c.abort();
Axios supports cancelling too (newer versions accept the same AbortController signal).
The browser reports an "unhandled promise rejection" in the console and the failure is easy to miss. Always add .catch() or try/catch around await. In a Vue app you can also add a global handler with window.addEventListener("unhandledrejection", ...) for logging.
Start a setInterval in mounted that calls the status endpoint and stores the result in data; stop it with clearInterval once the job is done and also in beforeDestroy. If you forget to clear it, the timer keeps running after the component is gone, causing memory leaks and extra requests.
mounted() { this.timer = setInterval(this.check, 3000); },
beforeDestroy() { clearInterval(this.timer); }It prints Promise { 5 } (a resolved promise), not the number 5, because async functions always wrap their return value. You need await f() or f().then(v => ...) to get 5.
An SPA loads one HTML page and then updates the view in the browser with JavaScript, getting data from APIs as JSON. A traditional multi-page app asks the server for a whole new HTML page on each navigation. SPAs feel faster after the first load and separate frontend from backend, but the first load is heavier and SEO needs extra work.
In server-side rendering (Flask with Jinja) the server builds the HTML and sends it ready to display. In client-side rendering (Vue) the server sends a small HTML shell plus JavaScript, and the browser builds the page from JSON API data. My project uses Vue for client-side rendering, with Flask serving only JSON APIs plus the static build.
A PWA is a web app that can be installed and can work offline or on poor networks. It needs HTTPS, a web app manifest (name, icons, start URL, display mode) and a service worker. The service worker is a script that runs in the background and can intercept network requests and serve cached files.
A service worker sits between the page and the network, so it can cache static files and respond when the user is offline, and it can handle push notifications. It runs in a separate thread, cannot access the DOM directly, works only over HTTPS (localhost is allowed), and has its own lifecycle of install, activate and fetch events. Because it can serve stale files, the cache must be versioned and old caches deleted on activate.
localStorage keeps strings with no expiry until cleared, per origin, about 5 MB, and is not sent to the server. sessionStorage is the same but lives only for that tab. Cookies are small (about 4 KB), can have an expiry, and are sent to the server automatically with every matching request; they can be marked HttpOnly so JavaScript cannot read them. Use cookies when the server must receive the value each time, and storage for client-only data.
IndexedDB is an asynchronous, transactional, in-browser database for large amounts of structured data, including files and blobs. localStorage is synchronous, string-only and small. Use IndexedDB for large offline data, for example cached lists in a PWA; use localStorage for tiny settings such as a theme or a token.
Ephemeral state is temporary UI state that is not worth saving: whether a dropdown is open, the current text typed in an input, scroll position or a loading spinner flag. It lives in component data and is lost on refresh, unlike persistent state such as user records, which belong in the database, or the token, which you may keep in localStorage.
I store it in localStorage (and also in Vuex so components can react to it), and send it in an Authorization or Authentication-Token header. The risk is that any injected script (XSS) can read localStorage and steal the token. An HttpOnly cookie cannot be read by scripts, but then you must defend against CSRF using SameSite and CSRF tokens. Either way, keep token lifetime short and escape all user content.
It can show UI, do calculations and keep data in the browser with localStorage or IndexedDB. But data is stored per browser and per user, it is not shared across devices or users, and the user can inspect or edit it. Secrets, access control and shared state need a server, since anything in client code is visible to the user.
Lighthouse is a Chrome DevTools audit tool that scores a page on Performance, Accessibility, Best Practices, SEO and (optionally) PWA checks. The performance score is computed from metrics such as First Contentful Paint, Largest Contentful Paint, Total Blocking Time, Cumulative Layout Shift and Speed Index. It gives suggestions like compressing images, removing unused JavaScript and caching static files.
The DOM is the browser's in-memory tree of objects built from the HTML, which JavaScript can read and modify. The HTML source is just text. After scripts change the DOM, what you see on screen can differ from the original file, which is exactly what Vue does when it renders.
Each HTTP request is independent, and the server does not remember earlier requests by itself. Apps add state on top: a session cookie holding an id that points to server-side session data, or a token sent by the client on each request that identifies the user. Token-based auth is common for SPAs and mobile apps because the API stays stateless.
The same-origin policy is a browser rule that stops a page from one origin from reading data from another origin. An origin is scheme + host + port, so http://localhost:8080 and http://localhost:5000 are different origins. It protects users from malicious sites reading their data on other sites; CORS is the controlled way for a server to relax it.
The server controls it with HTTP headers: Cache-Control (for example max-age=3600, no-store, no-cache, public, private), ETag and Last-Modified for validation, and Expires. With validators the browser sends If-None-Match or If-Modified-Since, and the server may reply 304 Not Modified with no body. This is separate from server-side caching with Redis.
Large JavaScript bundles and images, render-blocking scripts and CSS, many requests, slow server response (TTFB), no compression or caching, and layout shifts. These directly hurt metrics like LCP, TBT and CLS. Fixes are code splitting, lazy loading, compressing and resizing images, gzip or brotli, caching and a CDN.
Vue is a JavaScript framework for building user interfaces from components, with reactive data binding between state and DOM. "Progressive" means you can adopt it gradually: drop it in with one script tag to enhance a page, or use Vue CLI with the router and Vuex for a full SPA. This course uses Vue 2 (Options API); in newer Vue 3 there is also the Composition API.
CDN is just a script tag, with no build step, so it is quick for small pages and demos. Vue CLI gives single-file components (.vue), npm packages, hot reload, ES modules, linting and an optimised production build, which suits a multi-component SPA with Vue Router and Vuex. I would pick CLI for a project with many components and shared state, and say clearly which one I actually used and why.
v-if actually adds or removes the element from the DOM (and supports v-else-if and v-else), so it has a higher cost when toggling but nothing is rendered when false. v-show always renders the element and only toggles display: none, so toggling is cheap but the initial render cost is paid. Use v-show for something toggled often, such as a dropdown, and v-if when the condition rarely changes or the content is heavy. v-show does not work on <template>.
key gives each item a stable unique identity so Vue can reuse, move or remove the right DOM elements when the list changes, instead of patching in place and mixing up state (like input text). Use a unique id from the data, not the array index when items can be reordered or removed.
<li v-for="lot in lots" :key="lot.id">{{ lot.name }}</li>v-model is two-way binding on form inputs. On a text input it is shorthand for :value="x" plus @input="x = $event.target.value". It has modifiers .trim, .number and .lazy. On a custom component in Vue 2 it binds the value prop and the input event.
data holds the reactive state. computed properties are derived values that are cached and recalculated only when their reactive dependencies change. methods run every time they are called (and on each re-render if used in a template). watch runs a function for side effects when a property changes, such as calling an API when a search term changes.
Use computed because the result is cached and re-evaluated only when items or query change, whereas a method in the template would run on every re-render.
computed: {
filtered() {
return this.items.filter(i =>
i.name.toLowerCase().includes(this.query.toLowerCase()));
}
}
Computed must be pure: no API calls or data changes inside it.
Vue 2 walks through the data object and converts each property into a getter and setter using Object.defineProperty. When a render function or computed reads a property, Vue records the dependency, and when the setter runs it tells the dependents to re-run, so the DOM updates. In Vue 3 this uses Proxies instead.
Vue 2 cannot detect array index assignment or adding or deleting object properties because getters and setters are created only for properties existing at creation. Use this.$set(this.items, 0, x) (or Vue.set), splice, or replace the array or object with a new one: this.user = { ...this.user, age: 20 }. Declare all properties in data up front. (In Vue 3 Proxy-based reactivity removes this limitation.)
Components are reused, and if data were a shared object all instances would share and change the same state. A function returns a fresh object for each instance. The root instance created with new Vue can use an object because it exists only once.
beforeCreate, created (data and methods ready, no DOM yet; good for initial API calls), beforeMount, mounted (DOM is available; use for DOM access or third-party libraries), beforeUpdate, updated (after re-render), beforeDestroy (clean up timers and listeners) and destroyed. In Vue 3 the last two are renamed beforeUnmount and unmounted.
Usually in created or mounted. created fires earlier, before the DOM is mounted, so the request starts sooner and this data is already reactive. mounted is needed only if you must touch the DOM (for example to initialise a chart on a canvas). In both cases show a loading flag, because data arrives after the first render.
<div class="errorClass" :class="{ activeClass: isActive }"></div>
<div :class="['errorClass', { activeClass: isActive }]"></div>
A plain class attribute and a :class binding are merged by Vue. The object syntax adds the class when the value is truthy; an array can mix strings and objects.
: is shorthand for v-bind and @ for v-on. Mustache {{ }} interpolates text content only, while v-bind sets attributes and props, for example :href="url" or :disabled="busy". You cannot use mustaches inside attributes.
Modifiers change how an event handler behaves, without code in the method: @submit.prevent calls preventDefault, @click.stop stops propagation, .once runs only once, .self, .capture, and key modifiers like @keyup.enter. They keep methods free of DOM details.
It is a lightweight JavaScript tree describing what the UI should look like. On a state change Vue renders a new virtual tree, compares it to the previous one (diffing), and applies only the necessary changes to the real DOM, which is much cheaper than rebuilding everything.
Their priority differs between versions (v-for first in Vue 2, v-if first in Vue 3) and it also makes Vue loop over every item even when most are hidden. Instead, filter in a computed property and loop over the result, or move v-if to a wrapping <template>.
After you change data, Vue updates the DOM asynchronously in a batch. If you need to read or use the updated DOM, for example to focus an input that just appeared via v-if, call this.$nextTick(() => { ... }) (or await this.$nextTick()), which runs after the DOM update.
Add ref="name" to an element or component and access it as this.$refs.name after mount, for example this.$refs.search.focus(). Refs give direct access to the DOM or a child instance; prefer props and events for normal data flow because refs are not reactive.
Vue 3 adds the Composition API (setup, ref, reactive), Proxy-based reactivity, multiple root nodes in a template, better performance and TypeScript support, and its ecosystem is Vue Router 4 and Pinia (Vuex 4 still exists). Vue 2 uses the Options API and Object.defineProperty. This course works in Vue 2 with Vuex 3 and Vue Router 3.
The Vue app calls the Flask REST endpoints (for example /api/lots) using fetch or Axios, sends and receives JSON, and attaches the auth token in a request header. Flask returns JSON and status codes, and Vue stores the result in data or the Vuex store, which triggers a re-render. The frontend and backend are separate apps that share only the API contract.
export default {
data() { return { lots: [], loading: true, error: null }; },
async created() {
try {
const res = await fetch("/api/lots", {
headers: { "Authentication-Token": localStorage.getItem("token") },
});
if (!res.ok) throw new Error("HTTP " + res.status);
this.lots = await res.json();
} catch (e) { this.error = e.message; }
finally { this.loading = false; }
},
};
The template then uses v-if="loading", v-else-if="error" and v-for over lots.
Fetch is built into the browser, with no dependency, but it needs a manual res.json(), does not reject on HTTP errors, and has no interceptors. Axios parses JSON automatically, rejects on non-2xx, supports default headers, interceptors and cancellation, so it reduces repeated code. Either is fine; the point is to handle errors and attach the token consistently.
import axios from "axios";
const api = axios.create({ baseURL: "http://localhost:5000" });
api.interceptors.request.use(cfg => {
const t = localStorage.getItem("token");
if (t) cfg.headers["Authentication-Token"] = t;
return cfg;
});
api.interceptors.response.use(r => r, err => {
if (err.response && err.response.status === 401) {
localStorage.removeItem("token");
window.location = "/login";
}
return Promise.reject(err);
});
The request interceptor avoids repeating the header in each call, and the response interceptor handles expired tokens in one place.
Keep loading and error flags in data. Set loading to true before the call, then in finally set it to false, and store a message in error in the catch. In the template show a spinner with v-if="loading", an error message with v-else-if="error", and the list otherwise. This keeps the user informed and avoids a blank page.
Use the Vue CLI dev-server proxy so the browser sends requests to the same origin as the page, and the dev server forwards /api to Flask.
// vue.config.js
module.exports = {
devServer: { proxy: { "/api": { target: "http://localhost:5000", changeOrigin: true } } },
};
The browser sees only same-origin calls, so no CORS preflight is needed. In production, serve the built files and API from the same domain.
Run npm run build to produce static files in dist/. Flask can serve these as static files, with a route returning index.html for the root (and for all unknown paths when using history mode), while /api/* routes return JSON. Alternatively put Nginx in front: static files straight from disk and /api proxied to Flask.
Either serve the Vue page as a static file (not through Jinja), wrap the Vue part in {% raw %}...{% endraw %}, or change Vue's delimiters with delimiters: ["[[", "]]"] in the Vue instance. The cleanest solution is what we do in an SPA: Flask serves index.html as a static file and sends only JSON, so Jinja never touches the Vue templates.
A 401 means the token is missing, invalid or expired, so the app should clear the stored token and user from Vuex and localStorage and redirect to the login page, ideally in one central Axios or fetch wrapper. A 403 means the user is logged in but not allowed, so show a "not permitted" message and do not log out.
Send page, per_page and q as query parameters, fetch("/api/lots?page=2&q=park"), and let the backend apply LIMIT/OFFSET (for example paginate() in Flask-SQLAlchemy) and return the items with total count. The frontend keeps the page number in data or in the route query, and a debounced watcher on the search text reloads the first page. This keeps payloads small and fast.
For a file use FormData: const fd = new FormData(); fd.append("file", this.$refs.file.files[0]); await axios.post("/api/upload", fd); and do not set Content-Type manually, so the browser adds the multipart boundary. In Flask read it with request.files["file"]. For plain forms use @submit.prevent on the form and send JSON.
Put it in environment files such as .env.development and .env.production; Vue CLI exposes only variables prefixed with VUE_APP_, for example VUE_APP_API=http://localhost:5000, and you read them as process.env.VUE_APP_API. This keeps URLs out of the code, and secrets must never go there because they end up in the public bundle.
Actions centralise async logic and error handling, avoid repeating the same fetch in several components, and make the data available to every component through the store. The action calls the API and commits a mutation with the result, so components only dispatch the action and read state. For simple local data used by only one component, calling in the component is fine.
Jinja renders on the server: Flask fills in the template and sends finished HTML. Vue renders in the browser from JavaScript and JSON, so navigation and updates need no full reload. In my project Jinja is not used for pages, except possibly for email or PDF report templates generated on the server.
Down with props, up with events. The parent passes data through props (<child :title="t">), and the child sends a message by calling this.$emit("saved", payload), which the parent listens to with @saved="onSaved". This one-way flow keeps data changes traceable.
Props follow one-way data flow: the parent owns the data. If a child changes it, the parent's state is not updated, and the next re-render overwrites the change; Vue also warns about it. Instead, copy the prop into local data when you need a local editable copy, or emit an event so the parent updates its own state.
Vue.component("lot-card", {
props: { lot: { type: Object, required: true } },
template: `<div>{{ lot.name }}
<button @click="$emit('book', lot.id)">Book</button></div>`,
});
<lot-card v-for="l in lots" :key="l.id" :lot="l" @book="book"></lot-card>
book(id) in the parent receives the emitted id. Event names are best written in kebab-case in templates.
Slots let a parent pass template content into a child component. A default <slot></slot> renders whatever is placed between the child's tags, named slots (<slot name="header"> with <template v-slot:header>) place content in specific spots, and scoped slots let the child pass data back to the slot content. They make components flexible, like a card or modal with customisable content.
Declare props as an object with types, defaults and validators.
props: {
count: { type: Number, default: 0 },
status: { type: String, validator: v => ["open", "closed"].includes(v) },
user: { type: Object, default: () => ({}) },
}
Defaults for objects and arrays must be returned from a function. Invalid props only produce a console warning in development.
Vue.component("name", {...}) registers globally, so it can be used in any template but is always bundled. Local registration uses components: { Name } inside a component or view, so it is available only there and works better with tree shaking. Prefer local for components used in few places.
By default the component receives a value prop and emits an input event, so <my-input v-model="name"> is shorthand for :value="name" @input="name = $event". Inside the component you emit this.$emit("input", newValue). You can rename these with the model: { prop, event } option. Vue 2 also has the .sync modifier (:title.sync="t" with $emit("update:title", v)); in Vue 3 v-model uses modelValue and update:modelValue.
Move the shared state to their common parent and pass it down as props with events up; for many components, or deep trees, use a Vuex store. An event bus (a separate Vue instance with $on and $emit) works in small apps but becomes hard to track, and it was removed in Vue 3.
provide in an ancestor makes values available, and inject in any descendant reads them without passing props through each level (prop drilling). It is useful for library-style components such as tabs, but the data flow is less visible and the injected values are not reactive by default in Vue 2, so for app-wide state a Vuex store is clearer.
<style scoped> adds a unique data attribute to the component's elements and rewrites the CSS selectors so that the styles apply only to this component and do not leak. Child component root elements are also affected by the parent's scoped styles. To style deeper inside a child you use a deep selector (>>> or /deep/ in Vue 2).
A .vue file that contains a <template>, a <script> that exports the component options, and a <style>. A build tool such as webpack with vue-loader compiles it. SFCs keep markup, logic and styles of one component together and are why we need Vue CLI rather than the plain CDN.
key identifies the component to Vue's diff. If the key changes, Vue destroys the old instance and creates a new one, so its internal state is reset. For example <edit-form :key="user.id"> gives a fresh form each time the selected user changes.
<component :is="currentTab"></component> renders the component whose name or object is in currentTab, useful for tabs or wizards. Wrap it in <keep-alive> to keep a deactivated component's state instead of destroying it.
Make a Modal component with a <slot> for the body, a visible prop, and a close event. The parent controls visible with its own data and listens to @close.
<modal :visible="show" @close="show = false">
<p>Delete this lot?</p>
</modal>
This keeps the modal generic: the content comes from the slot and the state remains in the parent.
$parent couples the child tightly to a specific parent, so the component can no longer be reused elsewhere. $refs are not reactive and exist only after mounting. Props and events keep dependencies explicit and make components testable; use $refs only for direct DOM actions like focus.
Vue Router (version 3 for Vue 2) maps URL paths to components so a single-page app can have multiple "pages" with working back and forward buttons and shareable links, without reloading. You declare routes, add <router-view> where the matched component should appear, and navigate with <router-link> or this.$router.push.
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
const routes = [
{ path: "/", component: Home },
{ path: "/lot/:id", name: "lot", component: Lot, props: true },
{ path: "*", component: NotFound },
];
export default new VueRouter({ mode: "history", routes });
The router is passed to new Vue({ router, render: h => h(App) }). The wildcard route must be last.
<router-link to="/lots"> navigates inside the app through the history API without a page reload and keeps the Vue state, and it adds active classes automatically. A normal <a href> makes the browser request a new page from the server, reloading the whole app and losing the in-memory state.
Hash mode puts the route after # (/#/lots), which the server never sees, so it works with any static server. History mode uses clean paths (/lots) via the HTML5 history API, but a page refresh on /lots makes the browser ask the server for that path, so the server must fall back to index.html for unknown routes. In Flask add a catch-all route that returns index.html.
Params are part of the path: for /lot/:id read this.$route.params.id. Query is after ?: for /lots?page=2 read this.$route.query.page. Both come back as strings. If the same component is reused when only the param changes, the component is not recreated, so watch $route or use beforeRouteUpdate.
this.$router.push({ name: "lot", params: { id: 5 } });
this.$router.push({ path: "/lots", query: { page: 2 } });
this.$router.replace("/login");
this.$router.go(-1);
push adds a history entry, replace replaces the current one, and go(-1) goes back. When using path the params option is ignored, so use name with params, or build the path string yourself.
Guards run before a navigation completes and can allow, redirect or cancel it. A global beforeEach checks the token and role.
router.beforeEach((to, from, next) => {
const token = localStorage.getItem("token");
const role = localStorage.getItem("role");
if (to.meta.requiresAuth && !token) return next("/login");
if (to.meta.role && to.meta.role !== role) return next("/");
next();
});
Routes carry meta: { requiresAuth: true, role: "admin" }. The callback must always call next() exactly once.
No. A guard only controls the UI, and the user can edit the JavaScript, the localStorage values, or call the API directly with curl. Real security is on the server: every admin API verifies the token and the role (@roles_required("admin")) and returns 401 or 403. Front-end guards just give a good user experience.
A route can have children, which render inside a second <router-view> in the parent component, like a dashboard layout with a sidebar and a changing content area. Child paths are relative to the parent, for example /admin/users.
Use a dynamic import: { path: "/admin", component: () => import("./views/Admin.vue") }. Webpack splits that component into a separate chunk that downloads only when the route is visited, so the first load is smaller and faster. It is a good fit for rarely used pages like admin reports.
Use { path: "/home", redirect: "/" } for redirects, { path: "*", component: NotFound } as the last route to catch unknown paths, and name: "lot" on a route to navigate with { name: "lot", params: {...} } so changes to the path do not break links.
Set props: true on the route so :id arrives as a prop, props: route => ({ page: Number(route.query.page) || 1 }) for custom mapping. This decouples the component from $route, so it can be reused and tested with plain props.
Match the path to a route first, then read $route.params.flag as the string "true" (not the boolean) and $route.query.offset as the string "1". Then follow the template of that component, remembering that strings are always truthy and that {{ }} shows what is in params and query exactly. Check whether the path matches a nested child, or falls to the wildcard.
Vuex (version 3 for Vue 2) is a centralised store for shared state with strict rules for how it changes. Without it, passing the logged-in user, token or cached lists through many levels of props and events gets messy. With Vuex any component reads the same state and updates it only through mutations, so state changes are predictable and debuggable in devtools. For a small app with a few components, local state and props are enough.
state is the single source of data. getters are computed properties of the store (such as a filtered list). mutations are the only way to change state, and they must be synchronous. actions can be asynchronous (API calls), and they commit mutations. Components call this.$store.dispatch("fetchLots"), an action calls the API and does commit("SET_LOTS", data), and the mutation assigns state.lots.
The component dispatches an action; the action performs the async work (API request) and waits for it; the action commits a mutation with the result; the mutation changes the state; the components that use that state re-render. Only the mutation touches state directly.
Devtools record a snapshot before and after each mutation, which only works if the state change finishes at once. An asynchronous change would make it impossible to know when and why the state changed. Put async code in actions, and let the action commit when the result is ready.
export default new Vuex.Store({
state: { lots: [], token: localStorage.getItem("token") },
getters: { available: s => s.lots.filter(l => l.free > 0) },
mutations: {
SET_LOTS(state, lots) { state.lots = lots; },
},
actions: {
async fetchLots({ commit }) {
const res = await fetch("/api/lots");
commit("SET_LOTS", await res.json());
},
},
});
A component uses this.$store.state.lots, this.$store.getters.available and this.$store.dispatch("fetchLots").
They generate component members from the store so you do not write this.$store... repeatedly.
import { mapState, mapActions } from "vuex";
computed: { ...mapState(["lots"]) },
methods: { ...mapActions(["fetchLots"]) },
State and getters map to computed, while mutations and actions map to methods. The object spread operator is needed to combine them with other local properties.
Use this.$store.commit("update_total_cost", value) or ...mapMutations(["update_total_cost"]) and then call this.update_total_cost(value). Mutations are called with commit, never dispatch; dispatch is for actions. A mutation receives (state, payload), and when you need several values pass them as one object.
Vuex state lives in memory and is lost on refresh. So when logging in, also save the token (and role) to localStorage, and initialise the store from it: token: localStorage.getItem("token"). On logout clear both. A plugin like vuex-persistedstate can automate this.
When state is local to one component or passes only between a parent and a child, props and events are simpler. Vuex adds boilerplate, so it makes sense for data shared across many unrelated components: the current user, token, cart, notification counts and cached lists.
Modules split a large store into several parts such as auth and lots, each with its own state, mutations, actions and getters. With namespaced: true they are accessed as auth/login, so action names do not clash: this.$store.dispatch("auth/login", creds).
Direct changes bypass mutations, so devtools cannot track them, and in strict mode (strict: true, used in development) Vuex throws an error. Keeping all writes in mutations makes it easy to find who changed what. The component should commit a mutation or dispatch an action.
In newer Vue (Vue 3) the officially recommended store is Pinia, which has no mutations (you change state in actions or directly), simpler syntax and better TypeScript support. Vuex 4 still exists for Vue 3. Our course uses Vuex with Vue 2, so I explain state, getters, mutations and actions.
An action always returns a promise, so you can return values and the component can await it.
async login({ commit }, creds) {
const res = await axios.post("/login?include_auth_token", creds);
commit("SET_TOKEN", res.data.response.user.authentication_token);
return true;
}
In the component: const ok = await this.$store.dispatch("login", creds); if (ok) this.$router.push("/");. If the call throws, the rejection reaches the component's try/catch.
Put derived data that several components need in a getter, so the logic lives once and is cached while the state is unchanged. Use a component-level computed for logic that is specific to one component, such as sorting for its own view.
In session auth the server stores the session data and gives the browser a session id in a cookie, which is sent automatically and looked up on each request. In token auth the server issues a signed token after login, the client stores it and sends it in a header on every request, and the server verifies it without keeping session state. Tokens suit SPAs, mobile clients and multi-server setups; sessions are simple for server-rendered apps.
The user posts credentials to the login endpoint, the server verifies the hashed password and returns a token and role. Vue saves the token (Vuex plus localStorage), and every API call sends it in the Authentication-Token (Flask-Security) or Authorization: Bearer header (JWT). The server decorator @auth_required("token") or @jwt_required() checks it, and role decorators restrict admin routes. Logout removes the token from the client.
A JSON Web Token is header.payload.signature, three Base64URL parts. The header names the algorithm (like HS256), the payload carries claims such as sub, role, iat and exp, and the signature is made with the server's secret key. The payload is only encoded, not encrypted, so anyone can read it: never put passwords or secrets in it. The signature stops tampering.
It recomputes the signature over the header and payload with its secret key (HS256) or checks it with a public key (RS256), compares it with the signature in the token, and checks claims such as exp. If the signature is wrong or the token expired, it rejects with 401. No database lookup is needed, which is why JWTs are called stateless.
You enable it with a SQLAlchemyUserDatastore; the login endpoint (POST /login?include_auth_token, JSON body) returns an authentication token for the user. The client sends it in the Authentication-Token header, and routes use @auth_required("token") plus @roles_required("admin"). The token is a signed value checked by the server, and it can expire with the SECURITY_TOKEN_MAX_AGE setting.
from flask_security import auth_required, roles_required, current_user
@app.route("/api/admin/users")
@auth_required("token")
@roles_required("admin")
def users():
return jsonify([u.email for u in User.query.all()])
auth_required returns 401 when the token is missing or invalid, and roles_required returns 403 when the user lacks the role. current_user gives the logged-in user, so I can also scope data to the user.
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
@app.post("/api/login")
def login():
user = check_credentials(request.json)
if not user:
return {"msg": "bad credentials"}, 401
return {"token": create_access_token(identity=str(user.id))}
@app.get("/api/me")
@jwt_required()
def me():
return {"id": get_jwt_identity()}
The token lifetime is set with JWT_ACCESS_TOKEN_EXPIRES, and the signing key with JWT_SECRET_KEY.
localStorage is simple and works across tabs, but any XSS can read it. An HttpOnly, Secure, SameSite cookie cannot be read by scripts, but is sent automatically and so needs CSRF protection. I choose localStorage plus Vuex with a short expiry and escaped output, and explain this trade-off.
If a token is stolen, an expiry limits how long it can be used. A short-lived access token (minutes) is paired with a longer-lived refresh token that is used only to get a new access token. If the access token expires, the client uses the refresh token, or sends the user back to the login page. This balances security and convenience.
On the client, delete the token from Vuex and localStorage. The server cannot forget a stateless token, so it stays valid until it expires; to truly revoke it you keep a blocklist of revoked token ids (jti) in Redis until they expire, or use short lifetimes. That is a trade-off between stateless simplicity and revocation.
Passwords are never stored in plain text; they are salted and hashed with a slow algorithm such as bcrypt, PBKDF2 or Argon2 (Flask-Security does this through passlib). A rainbow table is a precomputed table of hashes for common passwords, used to reverse unsalted hashes. A random salt per password makes such tables useless, and a slow hash makes brute force expensive.
Authentication checks who you are (login with credentials or a token), authorisation checks what you may do. Role-Based Access Control grants permissions through roles such as admin, professional and customer, and each endpoint specifies which roles are allowed. It is easier to manage than permissions assigned per person.
401 Unauthorized means that authentication is missing or invalid, such as no token, a bad token or an expired token, so the client should log in again. 403 Forbidden means that the user is authenticated but not allowed to do this action, for example a customer calling an admin endpoint.
Cross-Site Request Forgery tricks a logged-in user's browser into sending an unwanted request to your site, such as a malicious page containing <img src="http://site/delete?id=1">. It works because browsers attach cookies automatically. A token stored in localStorage and sent manually in a header is not attached by the browser to a cross-site request, so the attack fails. If you use cookies for auth, use SameSite cookies and a CSRF token; the CSRF token is a random value the attacker's page cannot read, which the server must check.
Cross-Site Scripting injects attacker script into your page, for example through a comment containing <script>. Vue's {{ }} interpolation escapes HTML by default, so it is safe; v-html inserts raw HTML and must never be used with untrusted user content. Server-side, return JSON, not HTML, and sanitise rich text if you need to allow it.
Use the SQLAlchemy ORM or parameterised queries so user input is sent as bound parameters, never concatenated into SQL strings. For example User.query.filter_by(email=email) is safe, whereas db.session.execute("... WHERE email='" + email + "'") is vulnerable.
Use HTTPS, hash passwords with a salt, keep the secret key out of source control (environment variable), set a short token expiry, validate input, check roles on the server for every endpoint, check object ownership (a user can only see their own bookings), restrict CORS origins, and rate-limit the login endpoint.
In environment variables or a config file that is not committed to git, for example app.config["SECRET_KEY"] = os.environ["SECRET_KEY"]. A leaked signing key lets anyone forge tokens. Frontend environment variables are public in the bundle, so no secrets belong there.
The frontend might show the admin menu, but every API call goes to the server, which determines the role from the verified token and the database, not from what the client says, so admin endpoints return 403. This is why the role in localStorage is only for UI, and the server must be the real enforcement.
Browsers apply the same-origin policy: JavaScript on one origin cannot read responses from another origin. CORS (Cross-Origin Resource Sharing) is the mechanism by which a server tells the browser, through Access-Control-Allow-* response headers, which other origins may read its responses. It exists to protect users' data on other sites, and it is enforced by the browser, not the server.
An origin is the combination of scheme, host and port. https://example.com and https://example.com/page are the same origin, but http://example.com (different scheme), https://api.example.com (different host) and https://example.com:8443 (different port) are different origins. By default the browser allows fetch calls that read the response only to the same origin; others need CORS headers.
In development, either configure the backend with Flask-CORS to allow the Vue dev origin, or use the dev-server proxy so calls become same-origin. In production serve the frontend and API from the same domain, or allow exactly the frontend origin. Also make sure the Authorization or Authentication-Token header is allowed, because custom headers trigger a preflight.
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": ["http://localhost:8080"]}},
allow_headers=["Content-Type", "Authentication-Token", "Authorization"])
CORS(app) alone allows all origins, which is fine for local testing but should be limited in production. Flask-CORS adds the Access-Control-Allow-Origin header and answers the OPTIONS preflight requests.
For non-simple cross-origin requests (methods like PUT or DELETE, a JSON content type, or custom headers such as Authorization) the browser first sends an OPTIONS request asking if the real request is allowed. The server answers with Access-Control-Allow-Origin, Access-Control-Allow-Methods and Access-Control-Allow-Headers; only then the browser sends the actual request. Access-Control-Max-Age lets the browser cache the preflight result.
The CORS "simple request" rules allow GET, HEAD and POST with only a few content types (application/x-www-form-urlencoded, multipart/form-data, text/plain) and no custom headers. application/json and headers such as Authorization are outside that list, so the browser must ask permission first with an OPTIONS preflight.
No. CORS is enforced only by browsers to protect the user; the server still receives the request and curl or Postman ignore CORS completely. So CORS is not access control. Authentication and authorisation must still be done on the server.
It means that the response lacked the CORS header, so I check that the Flask-CORS or the headers are configured for this route and origin, that the preflight (OPTIONS) is handled and returns 200/204, and that the server did not crash with a 500 (error responses may lack CORS headers). Also check that the origin string matches exactly, including port and scheme. It works in Postman only because Postman does not enforce CORS.
For a cross-origin request to send and accept cookies, the client must opt in (fetch(url, { credentials: "include" }) or axios.defaults.withCredentials = true), and the server must send Access-Control-Allow-Credentials: true with a specific origin, because * is not allowed with credentials. The cookie's SameSite setting also must permit cross-site sending.
HttpOnly hides the cookie from JavaScript (document.cookie), reducing the impact of XSS. Secure sends it only over HTTPS. SameSite controls cross-site sending: Strict never sends it on cross-site requests, Lax sends it on top-level navigations with safe methods, and None (which requires Secure) allows all cross-site use. Together they protect against theft and CSRF.
A session cookie has no expiry date, so the browser normally discards it when the browser session ends, and it commonly holds just a session id that the server maps to stored data. Persistent cookies have an Expires or Max-Age. A session cookie is sent automatically with every request to its domain, which is why CSRF protection matters.
If any website could read credentialed responses, a malicious site could make the user's browser send their cookies and read the private response. So the spec requires an explicit origin in Access-Control-Allow-Origin when Allow-Credentials is true. Flask-CORS with supports_credentials=True echoes the specific allowed origin.
The attacker page includes <img src="http://example1.com/delete?id=1">; the browser sends the request with the victim's cookies, so the delete happens (CSRF). Prevent it by never using GET for state changes (use DELETE or POST), by requiring a CSRF token or a token header that a cross-site page cannot add, and by setting SameSite on cookies.
REST is an architectural style where resources are identified by URLs and manipulated through standard HTTP methods. It is stateless (each request carries all needed information), uses a uniform interface, and transfers representations such as JSON. Example: GET /api/lots, POST /api/lots, GET /api/lots/3, PUT /api/lots/3, DELETE /api/lots/3.
GET reads and is safe and idempotent. POST creates and is neither safe nor idempotent. PUT replaces a resource and is idempotent, as is DELETE; PATCH makes a partial update. Idempotent means repeating the request gives the same final state, which is why clients can safely retry PUT and DELETE.
200 OK, 201 Created for a successful POST, 204 No Content (for example a DELETE), 400 Bad Request for invalid input, 401 when not authenticated, 403 when not permitted, 404 when not found, 409 for a conflict such as a duplicate or an already-booked spot, 422 for validation errors in some APIs, and 500 for a server bug. Returning correct codes lets the frontend handle errors properly.
from flask_restful import Api, Resource, fields, marshal_with
api = Api(app)
lot_fields = {"id": fields.Integer, "name": fields.String, "price": fields.Float}
class LotList(Resource):
@marshal_with(lot_fields)
def get(self):
return Lot.query.all()
def post(self):
d = request.get_json()
lot = Lot(name=d["name"], price=d["price"])
db.session.add(lot); db.session.commit()
return {"id": lot.id}, 201
api.add_resource(LotList, "/api/lots")
marshal_with converts SQLAlchemy objects to JSON-safe dictionaries.
SQLAlchemy objects cannot be turned into JSON directly. We convert them to dictionaries with chosen fields, using marshal_with, Marshmallow schemas, or a to_dict() method, so we control what is exposed (never the password hash) and the format the frontend gets.
Read the JSON with request.get_json(), check required fields, types and ranges, and return 400 with a clear JSON message when invalid, before touching the database. Validate on the server even if the form is validated in Vue, because the client can be bypassed. Flask-RESTful's reqparse or Marshmallow can automate it.
Use query parameters: GET /api/lots?page=2&per_page=10&q=mall&sort=price. The server applies filters and paginate(), and returns a body like { "items": [...], "page": 2, "total": 57 }. Limiting per_page to a maximum protects the server.
HTTP clients, browsers, caches and tools like Axios understand status codes: with Axios a 4xx or 5xx rejects the promise, so error handling goes in the catch block. Always returning 200 hides failures and makes caching, monitoring and debugging harder.
Check and update in one database transaction, and let the database enforce it, for example with a unique constraint or SELECT ... FOR UPDATE style locking or an atomic conditional update (UPDATE ... WHERE status='free') and check the affected row count. If the second user's attempt finds the slot taken, return 409 Conflict. A simple "check then insert" without a transaction has a race condition. SQLite serialises writes, but the logic still needs to be atomic.
Versioning (like /api/v1/lots) lets you change the API without breaking existing clients, which keep using the old version. For a course project it is optional, but I would mention it as a good practice when clients such as a mobile app depend on the API.
PUT replaces the whole resource, so the client sends all fields; PATCH changes only the fields sent. Many projects use PUT for edits and just update the supplied fields. State which you use and be consistent; both should return the updated resource or 204.
Test with Postman, curl or the browser, and write down endpoint, method, body, auth header and response codes (for example in a README or an OpenAPI/Swagger file). Consistent documentation helps the frontend developer and the examiner understand the API contract.
Rate limiting caps how many requests a client can make in a period, protecting against abuse and brute force. It is most useful on the login endpoint and on expensive endpoints such as export. Flask-Limiter can do it, and it can use Redis to store the counters.
Do not keep per-client data in server memory between requests; each request carries the token and everything needed. Then any server instance can handle any request, so you can add more servers behind a load balancer. Shared state lives in the database or Redis.
The API is a contract between the two sides: the backend exposes data and operations as JSON endpoints, and any client (Vue app, mobile app, scripts) can use them. The teams can work independently and the backend can be reused, which is the reason to move from Jinja-rendered pages to Vue.
Redis is an in-memory key-value data store that supports strings, hashes, lists, sets and sorted sets, with optional persistence. In the project I used it in two roles: as the message broker (and result backend) for Celery, and as a cache for expensive API responses. Because data is in RAM, reads and writes are very fast.
Redis keeps data in memory, uses simple data structures with direct key access, and avoids query parsing and disk reads. A relational database stores data durably on disk with schemas, joins and transactions, and handles complex queries. Redis is used for fast, often temporary data (cache, queues, counters), the SQL database as the source of truth.
redis-cli ping # PONG
redis-cli set lots:1 "data"
redis-cli get lots:1
redis-cli expire lots:1 60
redis-cli ttl lots:1 # seconds left
redis-cli keys "*"
redis-cli del lots:1
SETEX key seconds value sets a value with expiry at once. KEYS * is fine for debugging but should not be used on big production databases (use SCAN).
A cache stores the result of an expensive operation, such as a database query or computation, so the next request can be answered quickly. This reduces response time and database load. The cost is that data may be stale, so we need an expiry and invalidation strategy.
With Flask-Caching using a Redis backend.
from flask_caching import Cache
cache = Cache(app, config={"CACHE_TYPE": "RedisCache",
"CACHE_REDIS_URL": "redis://localhost:6379/1",
"CACHE_DEFAULT_TIMEOUT": 300})
@app.get("/api/stats")
@cache.cached(timeout=60)
def stats():
return jsonify(compute_stats())
To prove it works: call the endpoint twice and show the second response is much faster, add a print in compute_stats that appears only once, and show the key with redis-cli keys "*" and its decreasing ttl.
cached builds the key from the request path (by default not from function arguments, and you can use query_string=True to include the query), so it suits a view with no arguments. memoize includes the function's arguments in the key, so get_user(1) and get_user(2) are cached separately, and you can invalidate them with cache.delete_memoized(func, args).
If the route has URL parameters, the default key is based on the request path, so each URL path gets its own entry: /compute/10/20 and /compute/5/10 are different keys, so each computes its own result on the first call and is cached after. If you wrongly used a fixed key_prefix (the same key for both), the second request would return the first request's cached response. Reading the key rule carefully decides the answer.
The expiry bounds how stale data can be and keeps memory from filling up. Fast-changing data such as available slots needs a short TTL (a few seconds or minutes), while rarely changing data such as a category list can use a long one. I choose the TTL according to how often the data changes and how bad stale data is.
Invalidation means removing or updating cached data when the underlying data changes, otherwise users see stale values. After a successful POST, PUT or DELETE I delete the related keys with cache.delete("lots") or cache.delete_memoized(get_lots), so the next read repopulates it. A TTL acts as the fallback in case invalidation is missed.
Include everything that makes the response different in the key: the user id (or role), the path, and the query parameters. For per-user data use key_prefix=lambda: f"bookings:{current_user.id}" or memoize with the user id as an argument. A key that omits the user would serve the first user's data to everybody, which is a serious leak.
A hit means the data is found in the cache; a miss means it is not, and we must load it from the database. In cache-aside the application first checks the cache; on a miss it reads the database, stores the result in the cache with a TTL and returns it. Writes update the database and invalidate the cache key.
import json, redis
r = redis.Redis(host="localhost", port=6379, db=1)
def get_lots():
cached = r.get("lots")
if cached:
return json.loads(cached)
data = [l.to_dict() for l in Lot.query.all()]
r.setex("lots", 60, json.dumps(data))
return data
After a write I call r.delete("lots"). Redis returns bytes, which json.loads accepts.
Highly dynamic or critical data where staleness is harmful, such as the real-time availability that decides if a booking succeeds (the booking check must hit the database); also responses with user-specific private data unless the key includes the user, and responses to POST or other state-changing requests.
By default it can be, unless persistence is enabled: RDB takes periodic snapshots, and AOF logs every write. For a cache we can accept loss because the data can be rebuilt from the database; for broker data (Celery queues) loss means pending tasks may disappear, so enable persistence if that matters.
No. HTTP caching happens in the browser or intermediate proxies controlled with Cache-Control, ETag and Expires headers, and avoids requests reaching the server at all. Redis caching is server-side: the request arrives, but the app avoids recomputing by reading Redis. They can be used together.
Redis has numbered databases (0 to 15 by default), so you can use redis://localhost:6379/0 for the Celery broker and redis://localhost:6379/1 for the cache. This keeps keys separate, so cache.clear() does not delete queued tasks. In production you might run separate instances.
The first visit computes and shows the time at 0 s. The second at 150 s is after the 120 s expiry, so it is a miss: it recomputes and shows the time at about 150 s and the new entry lasts until 270 s. The third at 210 s is within that window, so it shows the time from 150 s again. So the displayed times are 0, 150 and 150 seconds.
Redis can act as a message broker because its lists and publish/subscribe features can carry messages, which is how Celery uses it. It is also a cache and a data store with TTLs. It is not a replacement for a relational database for complex joins, and it is not "only" a cache.
Celery is a distributed task queue for Python. Your Flask app (the producer) puts a task message on a broker such as Redis, and a separate worker process picks it up and runs it in the background. We need it for slow work, such as exporting a CSV, sending emails or building reports, so that the HTTP request returns immediately and does not time out.
The application that defines tasks and sends them, the broker (Redis or RabbitMQ) that holds the queue of messages, one or more workers that execute tasks, an optional result backend (such as Redis) that stores results and states, and Celery Beat, a scheduler that sends periodic tasks. All of these run as separate processes.
from celery import Celery
celery = Celery("app", broker="redis://localhost:6379/0",
backend="redis://localhost:6379/0")
@celery.task
def add(x, y):
return x + y
celery -A app.celery worker -l info
In the Flask view call result = add.delay(2, 3) and return result.id. The -A argument points to the module and the Celery object.
An async task is triggered by a user or code event, such as the user clicking "Export CSV", through .delay(). A scheduled (periodic) task is triggered by time, like the daily reminder at 6 PM or the monthly report on the 1st, by Celery Beat. Both are executed by the same workers.
Beat is a scheduler process that reads a schedule (crontab or interval) and sends the due tasks to the broker at the right time. It does not execute the tasks; a worker must be running too. So for periodic jobs you start the worker and Beat as two processes (or one with -B in development).
from celery.schedules import crontab
celery.conf.timezone = "Asia/Kolkata"
celery.conf.beat_schedule = {
"daily-reminder": {"task": "tasks.send_reminders",
"schedule": crontab(hour=18, minute=0)},
"monthly-report": {"task": "tasks.monthly_report",
"schedule": crontab(day_of_month=1, hour=8, minute=0)},
}
celery -A app.celery beat -l info
Set the timezone explicitly, otherwise the schedule uses UTC.
The export endpoint starts the task and returns the task id immediately (202). The frontend polls a status endpoint that uses AsyncResult(task_id).state, and when it is SUCCESS it shows a download link, which hits a download endpoint that serves the file. Alternatives are SSE or a notification by email.
@app.get("/api/export/<task_id>")
def status(task_id):
r = celery.AsyncResult(task_id)
return {"state": r.state}PENDING (waiting or unknown), STARTED (if enabled), SUCCESS, FAILURE, RETRY and REVOKED. Note that PENDING is also reported for an unknown task id. The state is stored in the result backend.
The task runs in the worker process, outside a Flask request, so db, current_app and config are not available automatically. Create the Flask app and push an application context inside the task, or subclass celery.Task with a context wrapper.
class ContextTask(celery.Task):
def __call__(self, *a, **kw):
with app.app_context():
return self.run(*a, **kw)
celery.Task = ContextTaskUse bind=True and call self.retry on a recoverable error, or autoretry_for.
@celery.task(bind=True, max_retries=3, default_retry_delay=60)
def send_mail(self, to):
try:
smtp_send(to)
except ConnectionError as e:
raise self.retry(exc=e)
Because a task can run more than once, it should be idempotent, so that a retry does not send duplicates or double-update.
delay(*args, **kwargs) is a shortcut for apply_async(args, kwargs). apply_async accepts options like countdown=60 (run after 60 seconds), eta, expires, queue and retry. Both return an AsyncResult immediately without waiting for the task.
get() blocks until the task finishes, so the request would hang and time out, which defeats the purpose of a background job. Return the id immediately and let the client poll or be notified.
Redis is easy to install and already used as a cache, so the project needs one service for both. RabbitMQ is a dedicated message broker with stronger delivery guarantees and routing features. For a course project Redis is simple and sufficient.
Redis has no official Windows build, and Celery 4 and later dropped official Windows support, as its default prefork pool does not work reliably there. WSL provides a Linux environment where both run normally. (Celery can be run on Windows with --pool=solo, only for development.)
Long-running or heavy work that the user should not wait for: report generation, data analysis, sending bulk emails, image processing, calling slow external services, and scheduled jobs. It is not needed for quick operations like a simple database read.
The client sends the request; the API validates it and puts a task on the broker queue; the API immediately returns a task id (HTTP 202); a worker takes the task and runs it; the result is stored in the result backend or database; the client polls (or is notified) and fetches the result. The key point is the API never blocks on the heavy work, and you scale by adding workers.
Messages accumulate in the queue, so the queue grows (and latency increases). You fix it by adding more consumers (workers), speeding up the task, or limiting the producer. The broker's memory can eventually run out if the backlog keeps growing.
One task with a loop is simple but runs sequentially in one worker and a failure may stop the rest. One subtask per user, started from a parent via group() or a loop of .delay(), runs in parallel across workers and retries failures individually, at the cost of more messages. For 1000 emails the per-user tasks are usually better; just limit the rate for the SMTP server.
Run more worker processes or machines pointed at the same broker, and raise the concurrency (-c 4). Use separate queues for different kinds of tasks, so slow reports do not block quick emails. Only one Beat scheduler should run, otherwise periodic tasks are sent twice.
A webhook is an HTTP callback: when an event happens, the source system sends an HTTP POST with the event data to a URL that you registered. It is "reverse API" or "don't call us, we'll call you", so you do not need to poll. Examples: payment gateway notifications, or posting a message to Google Chat or Slack with their incoming webhook URL.
# sender: post a reminder to a chat webhook
import requests
requests.post(WEBHOOK_URL, json={"text": "Your parking ends in 10 minutes"})
# receiver: a Flask route
@app.post("/webhook/payment")
def payment_hook():
event = request.get_json()
process(event)
return "", 204
The receiver should respond quickly (do heavy work in Celery) with a 2xx code, otherwise the sender may retry.
Use HTTPS, verify a shared secret or an HMAC signature header computed from the raw body, compare with hmac.compare_digest, validate the payload, ignore replays (timestamps or event ids), and make the handler idempotent because senders retry. Never trust a request just because it reaches your URL.
Webhooks are event-driven: low latency, no wasted requests, but the receiver must be publicly reachable and handle retries. Polling is simple and works behind firewalls and when the source offers no webhooks, but it wastes requests and has delay up to the polling interval.
SSE is a one-way stream from server to browser over a normal HTTP connection with the text/event-stream content type. The browser uses EventSource, which reconnects automatically and can resume with Last-Event-ID. Each message is a block of text lines ending with a blank line, such as data: hello.
import time
from flask import Response
@app.get("/stream")
def stream():
def gen():
while True:
yield f"data: {time.time()}\n\n"
time.sleep(2)
return Response(gen(), mimetype="text/event-stream")
const es = new EventSource("/stream");
es.onmessage = e => console.log(e.data);
Each message must end with a blank line. The server needs a threaded or async worker so a stream does not block other requests.
Polling asks repeatedly and is simple but wasteful. Long polling holds the request open until data is ready or a timeout. SSE is one-way server to client with automatic reconnect over HTTP. WebSocket is a two-way full-duplex channel over a single connection, better for chat and games.
The standard EventSource API does not allow custom headers. So send the token as a query parameter (use a short-lived, single-purpose token, since URLs can be logged), or use cookies, or use fetch streaming. This detail matters if your SSE endpoint is protected.
In short polling the client sends requests at fixed intervals and the server answers immediately, even with "no news". In long polling the server keeps the request open until it has new data or a timeout, then the client immediately asks again. Long polling reduces empty responses but holds server connections.
In publish/subscribe a publisher sends a message to a topic and every subscriber of that topic receives a copy. In point-to-point (a queue) each message is delivered to exactly one consumer, which is how Celery workers share tasks. A message broker can support both.
It decouples producer and consumer, so they need not be online together, it buffers bursts of work, allows load to be shared across multiple workers, enables retries and routing, and lets you scale parts independently. The cost is extra infrastructure and eventual (not immediate) results.
Use Python's smtplib with email.message.EmailMessage, or Flask-Mail, connecting to an SMTP server.
import smtplib
from email.message import EmailMessage
def send_mail(to, subject, html):
msg = EmailMessage()
msg["From"], msg["To"], msg["Subject"] = "app@example.com", to, subject
msg.set_content("Please view this in an HTML client")
msg.add_alternative(html, subtype="html")
with smtplib.SMTP("localhost", 1025) as s:
s.send_message(msg)
Sending is slow, so call it from a Celery task.
MailHog is a fake SMTP server that accepts all mail and shows it in a web UI (port 8025, SMTP on 1025) so you can see emails without sending real ones and without credentials. In production you would swap the host and port to a real SMTP provider with TLS and authentication details taken from environment variables.
A Celery beat task queries the month's data per user, renders an HTML report with a Jinja template, and sends it as the HTML body. For the PDF convert the same HTML using a library like WeasyPrint, pdfkit (wkhtmltopdf) or xhtml2pdf, and attach the bytes with msg.add_attachment(pdf, maintype="application", subtype="pdf", filename="report.pdf").
Send them from background tasks, one task per recipient (or in batches) so failures are retried individually, and rate-limit to respect the SMTP provider's limits. Reuse the SMTP connection within a batch, and log failures. Avoid sending in the web request.
Both are server-initiated notifications, but differ: webhooks are server-to-server HTTP POSTs to a registered URL, while SSE is a long-lived server-to-browser stream. Webhooks need a publicly reachable receiver; SSE uses a normal browser connection and EventSource, and is one-directional. Neither is bidirectional like WebSockets.
I measured the response time with the browser DevTools Network tab, a simple timer, or a load tool before and after changes, and ran Lighthouse for the frontend. Improvements: Redis caching of expensive endpoints, background jobs for heavy work, database indexes and pagination, lazy-loaded routes, compressed and sized images. Always say what you measured and the numbers.
Call the same endpoint twice and compare the times in the Network tab or with curl -w "%{time_total}": the first (miss) is slow and the second (hit) is fast. Also show the key in redis-cli, and show that a database query log or a print statement appears only for the miss. Then change the data and show invalidation or the TTL expiry.
It is when you run one query to get N parent rows and then one more query for each parent to get its related rows, giving N+1 queries. In SQLAlchemy fix it with eager loading such as joinedload or selectinload, or a single JOIN query. It is a common reason for slow list endpoints.
An index is an additional data structure (usually a B-tree) that speeds up lookups, filters and sorts on a column, like a book index. The cost is extra storage and slower writes because the index must also be updated. Index columns used often in WHERE, JOIN or ORDER BY.
Code splitting and lazy-loaded routes, minified bundles, removing unused libraries, compressing and lazily loading images, gzip or brotli, HTTP caching for static files, a CDN, and avoiding render-blocking resources. These improve metrics measured by Lighthouse like LCP and TBT.
First Contentful Paint (FCP), Largest Contentful Paint (LCP), Total Blocking Time (TBT), Cumulative Layout Shift (CLS) and Speed Index are the main ones. Things like Time to First Byte, bundle size and image weight influence them indirectly; accessibility or SEO checks don't change the performance score.
Vertical scaling gives one machine more CPU or RAM; it is simple but has a limit and a single point of failure. Horizontal scaling adds more machines behind a load balancer, which needs stateless application servers and a shared database or cache. Statelessness (token auth, Redis for shared state) makes horizontal scaling possible.
It is single-process and not designed for security, concurrency or reliability. Use a WSGI server such as Gunicorn with several workers behind Nginx, which serves static files, terminates TLS and proxies /api to Flask. This also lets many requests run in parallel.
Use a tool such as Apache Bench (ab -n 1000 -c 20 http://localhost:5000/api/lots), Locust or k6, and watch the response times, the error rate and the requests per second, with and without caching. The goal is to find the bottleneck, so you test one change at a time.
They limit how often an expensive handler runs. Debounce waits until the events stop (search while typing), throttle runs at most once per interval (scroll or resize handlers). Both reduce API calls and re-renders.
Computed values are cached based on their reactive dependencies, so they do not recompute on every render unless data changes, while a method call in the template runs on every re-render. For expensive derived data such as a filtered and sorted list, computed saves work.
No. Caching adds complexity: stale data, invalidation bugs, memory use, and the risk of serving one user's data to another if keys are wrong. It helps most for data that is read often, expensive to compute, and changes rarely. Measure first, then cache the real bottleneck.
A Vue 2 single-page app (Vue CLI, Vue Router, Vuex) talks to a Flask REST API with token authentication and SQLAlchemy over SQLite. Redis serves as the cache and as the Celery broker; Celery workers run async jobs (CSV export), and Celery Beat triggers the daily reminders and monthly reports sent by email. Role-based access separates the admin and user. I explain this flow end to end with the main files of each part.
My app has many components and shared state, so I needed single-file components, npm packages (axios, vue-router, vuex), hot reloading and a production build. The CDN version is simpler, but all templates would be strings or in one HTML page and it does not scale to many components. The trade-off is a build step and Node tooling.
Several unrelated components need the same data: the logged-in user, role and token, and some shared lists. Passing them through props and events would be tangled, so a store gives one source of truth. Auth state lives in Vuex (synced to localStorage for refresh), and API calls are in actions. If it were a tiny app, I would not use Vuex.
The frontend is a separate SPA, so a token in a header works the same for the SPA, curl or a future mobile app, and the API stays stateless. I used the token issued by Flask-Security (or JWT) and enforced it with @auth_required("token"). Cookies and sessions would need CORS credentials and CSRF handling.
Users have roles (admin, customer and so on) stored in the database with Flask-Security's user-role tables. Each API endpoint is protected with @auth_required("token") and @roles_required("admin") as needed, and customer endpoints filter by current_user.id so users only see their own data. The Vue router guards use the role to hide pages, but the server is the real enforcement.
A Celery Beat entry with crontab(hour=18, minute=0) triggers a task each day. The task queries users who have not visited or have pending items, builds a message and sends an email (or a Google Chat webhook message) through SMTP. It runs in a worker, uses the application context, and I tested it with a short schedule (every minute) before setting the real time.
A Beat task with crontab(day_of_month=1, ...) runs on the first of each month. For each user it aggregates last month's data with SQL (counts, totals, most used items), renders an HTML template, and emails it (optionally with a PDF attachment). Heavy aggregation and email run in the Celery worker so the web server is not affected.
The user clicks Export and the API starts a Celery task with .delay() and returns a task id at once. The task queries the data, writes a CSV file and saves it (or emails a link). The frontend polls the status endpoint using the task id and shows a download button when the state is SUCCESS. This avoids request timeouts for large exports.
I cached read-heavy endpoints, such as the list of lots or the dashboard statistics, in Redis with a TTL chosen from how fast the data changes (60 seconds for availability, longer for static data). Write endpoints invalidate the affected keys, and keys include the user id when the response is user-specific. If asked, I demonstrate it with the timing and redis-cli.
The server sets a maximum token age; when the token expires the API responds with 401. The frontend's response interceptor catches the 401, clears the store and localStorage, and redirects to login with a message. A short expiry limits the damage of a stolen token; for better usability a refresh token could be added.
Tables with relationships (users, roles, and the domain entities with foreign keys) defined using SQLAlchemy models, with unique constraints and indexes on frequently queried columns. SQLite was required, needs no setup and is fine for a single-user demo; its limits are weak write concurrency and no network access, so for production I would use PostgreSQL.
Hashed and salted passwords, token authentication with expiry, server-side role checks, input validation, ORM queries against SQL injection, Vue's output escaping against XSS, a limited CORS origin list, secrets in environment variables, and no sensitive data in the JWT payload. I would add HTTPS, rate limiting and refresh tokens in production.
Pick a real one, such as getting Celery and Redis to run together, debugging CORS and preflight errors, or keeping cache and database consistent. Describe the symptom, how you found the cause (logs, Network tab, redis-cli), and the fix. A concrete debugging story shows that you built it yourself.
Refresh tokens and token revocation, pagination everywhere, automated tests, Docker Compose for the services, PostgreSQL, rate limiting, better error handling and a CI pipeline. Naming trade-offs honestly shows understanding.
Start Redis, run the Flask server, start a Celery worker and Celery Beat, start MailHog for emails, and run the Vue dev server (npm run serve) or serve the build. I keep these commands in the README, and I can show each process's log when a task runs.
Use a stateless Flask app behind Nginx with several Gunicorn workers, move from SQLite to PostgreSQL, run multiple Celery workers, and use Redis for caching and shared state. Serve the Vue static build through a CDN. Because token auth is stateless, adding servers is straightforward.
Be ready to open any file and explain it line by line: the model, an API endpoint, a Vuex action, a Celery task. If you used a library or a template, say so and explain what it does. Examiners can ask you to change something live, so understand your code, not just run it.
It is simple, survives refreshes, and works with the Authorization header. I know the risk is XSS, so I escape output, avoid v-html with user data, and use a short expiry. An HttpOnly cookie is an alternative that needs CSRF protection and CORS credentials.
The API returns proper status codes and a JSON message, Axios or fetch errors are caught in actions or components, and the UI shows the message (a toast or an alert) and keeps the form data. Specific cases such as 401 trigger a redirect to login, and a failed Celery task is logged and, if it matters, retried.
Add a method and a button in the component.
<button @click="load">Refresh</button>
methods: {
async load() {
this.loading = true;
const res = await fetch("/api/lots");
this.lots = await res.json();
this.loading = false;
}
},
created() { this.load(); }
Reusing the same load method in created avoids duplicate code. Explain that the data assignment triggers a re-render.
<p v-if="!loading && lots.length === 0">No items found</p>
<ul v-else>
<li v-for="l in lots" :key="l.id">{{ l.name }}</li>
</ul>
v-else must follow the v-if element immediately. Use lots.length === 0 because an empty array is truthy.
<input v-model="query" placeholder="Search">
<li v-for="l in filtered" :key="l.id">{{ l.name }}</li>
data() { return { query: "", lots: [] }; },
computed: {
filtered() {
const q = this.query.toLowerCase();
return this.lots.filter(l => l.name.toLowerCase().includes(q));
}
}
I use computed so it is cached and updates as query changes.
Add meta to the route and check it in the guard.
{ path: "/reports", component: Reports, meta: { requiresAuth: true, role: "admin" } }
The existing beforeEach redirects when the role does not match. Then add @roles_required("admin") on the backend endpoint, because the guard alone is not security.
@app.get("/api/admin/count")
@auth_required("token")
@roles_required("admin")
def count():
return {"count": Lot.query.count()}
Then test it in Postman with and without the token and as a non-admin to show 401 and 403. Mention that the decorator order matters: @app.get must be the outermost.
Change @cache.cached(timeout=60) to timeout=10, restart Flask, and call the endpoint repeatedly. For the first 10 seconds the response is identical (hit); afterwards a recomputed value appears (miss). redis-cli ttl <key> shows the counting down. A shorter TTL gives fresher data but more database work.
In the POST handler, after the commit, delete the cached key.
db.session.add(lot); db.session.commit()
cache.delete("view//api/lots") # key used by the cached list view
In Flask-Caching the default key for cached is view/<request.path> (prefix view/), so inspect the key with redis-cli keys "*" or give the decorator an explicit key_prefix="lots" and delete that. Then GET again to show the new item appears.
Change the schedule in beat_schedule.
"daily-reminder": {"task": "tasks.send_reminders",
"schedule": crontab(minute="*/2")}
Restart Beat (the worker picks the task up automatically), and show the log lines appear every 2 minutes and the mail in MailHog. Using schedule: 120.0 (seconds) also works.
@celery.task
def welcome_mail(to):
send_mail(to, "Welcome", "<h1>Hello</h1>")
@app.post("/api/welcome/<int:uid>")
@auth_required("token")
def welcome(uid):
user = User.query.get_or_404(uid)
welcome_mail.delay(user.email)
return {"msg": "queued"}, 202
Restart the worker, because it only knows tasks loaded at startup, then show the mail in MailHog.
state: { items: [] },
mutations: { ADD(state, item) { state.items.push(item); } },
getters: { total: s => s.items.reduce((t, i) => t + i.price, 0) },
this.$store.commit("ADD", { name: "Pen", price: 10 });
// template: {{ $store.getters.total }}
The getter recalculates automatically when items changes.
The data arrives asynchronously, so on the first render user is still null or {} and user.address.city throws or shows nothing. Initialise data with the right shape, or guard the template with v-if="user", or use optional access in a computed property (this.user && this.user.address). In Vue 2 templates the ?. operator is not supported.
<button :disabled="busy" @click="save">Save</button>
async save() {
this.busy = true;
try { await axios.post("/api/lots", this.form); }
finally { this.busy = false; }
}
Using finally makes sure the button is enabled again even when the call fails. It also prevents double submission.
Add loading and error to data, set them in the load method with try/catch/finally, and write <p v-if="loading">Loading...</p><p v-else-if="error" class="err">{{ error }}</p> before the list. Use v-else-if/v-else so only one block displays at a time.
Install Flask-CORS and enable it for the frontend origin and allow the needed headers.
from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "http://localhost:8080"}},
allow_headers=["Content-Type", "Authentication-Token"])
Restart Flask and check in the Network tab that the response has Access-Control-Allow-Origin. If the origin or port is different, the header will not match.
Read the role from the store and use v-if.
<router-link v-if="role === 'admin'" to="/admin">Admin</router-link>
<router-link v-if="token" to="/profile">Profile</router-link>
computed: { ...mapState(["role", "token"]) }
Mention that this only hides links; the API must still check the role.
@app.get("/api/lots")
def lots():
page = request.args.get("page", 1, type=int)
p = Lot.query.paginate(page=page, per_page=10, error_out=False)
return {"items": [l.to_dict() for l in p.items], "page": p.page, "pages": p.pages}
In Vue keep a page in data, call /api/lots?page=${this.page} and disable Previous on page 1 and Next on the last page.
Add the column to the SQLAlchemy model, then update the database: SQLite will not change an existing table with create_all, so use a migration (Flask-Migrate) or recreate the DB in development. Then update the API's input handling and the output fields, the Vue form (v-model), and any validation. Forgetting the database migration is the common mistake.
// child
methods: { remove() { this.$emit("deleted", this.item.id); } }
<!-- parent -->
<item-row v-for="i in items" :key="i.id" :item="i" @deleted="onDeleted"></item-row>
onDeleted(id) { this.items = this.items.filter(i => i.id !== id); }
The parent owns the list, so it does the removal, and the child just reports the event.
Save it to localStorage when it changes and read it on load.
data() { return { theme: localStorage.getItem("theme") || "light" }; },
watch: { theme(v) { localStorage.setItem("theme", v); } }
Mention that localStorage holds strings and is per-origin, and that this is non-sensitive data.
watch: {
query() {
clearTimeout(this.t);
this.t = setTimeout(this.search, 300);
}
},
methods: { async search() { /* call /api/search?q=this.query */ } }
Without the debounce each keystroke would send a request. Clear the timer in beforeDestroy.
For Flask-Security set SECURITY_TOKEN_MAX_AGE = 1800 (seconds) in the config; for Flask-JWT-Extended set JWT_ACCESS_TOKEN_EXPIRES = timedelta(minutes=30). Restart the server. After 30 minutes the API returns 401, and the frontend interceptor logs the user out. To demo, use a small number like 30 seconds.
Use self.update_state with a custom state and meta in a bound task, and read it in the status endpoint.
@celery.task(bind=True)
def export(self):
for i, row in enumerate(rows):
write(row)
self.update_state(state="PROGRESS", meta={"done": i, "total": len(rows)})
The status endpoint returns r.state and r.info, and the frontend shows a progress bar.
key = f"fail:{email}"
n = r.incr(key)
r.expire(key, 600)
if n > 5:
return {"msg": "Too many attempts"}, 429
INCR is atomic, and the expiry resets the window after 10 minutes. Delete the key after a successful login.