Checkpoint: A Dynamic Web App
Reviewed & published by Brayan K
This checkpoint ties together everything you've learned — templates, url_for, cookies, uploads, config, contexts, hooks, and class-based views — into one small dynamic web app.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
Instead of new theory, you'll recap the core ideas of this section, then prove you can combine them: a build challenge with a runnable starter and full solution, a short quiz, and a recap pointing to what comes next.
What You'll Bring Together in This Checkpoint
- Render dynamic HTML with Jinja2 templates and inheritance
- Build links with url_for and send users on with redirects
- Remember visitors using cookies on the response
- Accept and validate user file uploads safely
- Use the app and request context, plus g for per-request data
- Hook into requests with before_request / after_request
- Group a resource's logic in a MethodView class-based view
- Manage settings cleanly with Flask configuration
🛠️ Build Challenge: A Tiny Dynamic App
Your task is to build one small app that combines four ideas from this section into a single working page. None of the pieces are large on their own — the skill is wiring them together so they cooperate on every request.
- A templated page. Render HTML with render_template_string, passing in a title and a list of tasks, and loop over the tasks with a Jinja2 {% for %} block.
- A cookie visit counter. Read a visits cookie (default "0"), add one, show it on the page, and write the new value back with set_cookie.
- A request hook. Use after_request to add a custom header (for example X-Request-ID) to every response, reading the value off g that a before_request hook set.
- A class-based view. Serve the page from a MethodView subclass's get method, registered with app.add_url_rule(...) and as_view(...).
Start from the scaffold below. It already runs and prints output — your job is to replace the TODO comments so the counter increments and the header comes from g. Run it as you go to see your progress.
# 🛠️ BUILD CHALLENGE — starter scaffold (runs as-is; finish the TODOs)
from flask import Flask, render_template_string, make_response, request, g
from flask.views import MethodView
app = Flask(__name__)
# A tiny inline template. TODO: add a line that shows {{ visits }}.
PAGE = "<h1>{{ title }}</h1><p>Tasks: {{ tasks|length }}</p>"
@app.after_request
def add_header(response):
# TODO: read the id off g (set it in a before_request hook) instead of a constant.
response.headers["X-Request-ID"] = "req-001"
return response
class HomeView(MethodView):
def get(self):
# TODO: read the "visits" cookie, increment it, and set it back below.
html = render_template_string(
PAGE,
title="My Tasks",
tasks=["write code", "test code", "ship it"],
)
resp = make_response(html)
return resp
app.add_url_rule("/", view_func=HomeView.as_view("home"))
# Drive it with the test client, no server needed:
with app.test_client() as client:
r = client.get("/")
print("Status:", r.status_code) # 200
print("X-Request-ID:", r.headers["X-Request-ID"]) # req-001
print("Body:", r.data.decode()) # <h1>My Tasks</h1><p>Tasks: 3</p>Out of the box this prints a 200 status, the placeholder header, and the rendered task list. Once you finish the TODOs, the body should also report the visit number, and the header should come from g.
# ✅ FULL SOLUTION — template + cookie + hook + MethodView
from flask import Flask, render_template_string, make_response, request, g
from flask.views import MethodView
app = Flask(__name__)
# 1) A templated page: a heading, the visit count, and a task list.
PAGE = (
"<h1>{{ title }}</h1>"
"<p>Visits: {{ visits }}</p>"
"<ul>{% for t in tasks %}<li>{{ t|capitalize }}</li>{% endfor %}</ul>"
)
# 3) Request hooks: stash an id on g, then add it as a header on the way out.
@app.before_request
def set_request_id():
g.request_id = "req-001"
@app.after_request
def add_header(response):
# after_request MUST return the response.
response.headers["X-Request-ID"] = g.get("request_id", "?")
return response
# 4) A class-based view: GET is dispatched to get().
class HomeView(MethodView):
def get(self):
# 2) Cookie visit counter (cookies are strings, so convert with int()).
visits = int(request.cookies.get("visits", "0")) + 1
html = render_template_string(
PAGE,
title="My Tasks",
visits=visits,
tasks=["write code", "test code", "ship it"],
)
resp = make_response(html)
resp.set_cookie("visits", str(visits))
return resp
app.add_url_rule("/", view_func=HomeView.as_view("home"))
# Drive it with the test client (it remembers cookies between calls):
with app.test_client() as client:
r1 = client.get("/")
print("X-Request-ID:", r1.headers["X-Request-ID"]) # X-Request-ID: req-001
print("Visits=1 in body:", "Visits: 1" in r1.data.decode()) # True
r2 = client.get("/")
print("Visits=2 in body:", "Visits: 2" in r2.data.decode()) # True🔬 How the Four Pieces Cooperate
| Piece | Runs when | Key call |
|---|---|---|
| before_request hook | Before every view | g.request_id = ... |
| MethodView.get | On a GET to the route | render_template_string(...) |
| Cookie counter | Inside the view | resp.set_cookie("visits", ...) |
| after_request hook | After every view | response.headers[...] = ... |
The request flows top to bottom: the before_request hook fires first, the view builds the page and cookie, and the after_request hook stamps the header just before the response leaves.
⚙️ Review: A Config Loader
Configuration is the one piece from this section the build challenge did not touch. app.config.from_object() copies only the UPPER_CASE attributes of a class, so a ProductionConfig can inherit safe defaults and override just what must change. Secrets then come from the environment, never from code. This runs as plain Python: run it and compare the three lines it prints with the two classes.
# WORKED EXAMPLE: how app.config.from_object() and an environment override
# combine, in plain Python. A config is just a dict of UPPER_CASE keys.
class Config: # defaults, safe for development
DEBUG = True
SECRET_KEY = "dev-only"
MAX_CONTENT_LENGTH = 1024 * 1024 # uploads capped at 1 MB
class ProductionConfig(Config): # inherits, then overrides what must change
DEBUG = False
def from_object(obj):
# from_object copies only the UPPER_CASE attributes, exactly like this:
return {key: getattr(obj, key) for key in dir(obj) if key.isupper()}
env = {"SECRET_KEY": "s3cret-from-env"} # stands in for os.environ
config = from_object(ProductionConfig)
# A secret must never live in code: prefer the environment when it is set.
config["SECRET_KEY"] = env.get("SECRET_KEY", config["SECRET_KEY"])
for key in sorted(config):
print(f"{key} = {config[key]!r}")
# ✅ Expected output:
# DEBUG = False
# MAX_CONTENT_LENGTH = 1048576
# SECRET_KEY = 's3cret-from-env'🎯 Your Turn: url_for, a Cookie Counter, Upload Checks
Three ideas from this section that the build challenge left out, as plain Python that runs anywhere: url_for looking a route up by its endpoint name, a cookie counter that must convert its string value before adding, and the upload extension check. Fill the three blanks, then run and compare with the expected output.
# 🎯 YOUR TURN — three ideas from this section in plain Python: url_for's
# route lookup, a cookie visit counter, and upload validation.
# Fill in the three ___ blanks, then press Run.
# 1) URL_FOR: the app keeps a map of endpoint name -> URL rule. url_for fills
# the <parameters> in, so links never hard-code a path.
RULES = {"home": "/", "task_detail": "/tasks/<int:task_id>", "static": "/static/<path:filename>"}
def url_for(endpoint, **values):
url = RULES[endpoint]
for name, value in values.items():
for converter in ("int", "path", "string"):
url = url.replace(f"<{converter}:{name}>", str(value))
return url
print(url_for("home"))
print(url_for(___, task_id=7)) # 👉 replace ___ with "task_detail"
# 2) COOKIES: cookies are always strings, so convert before adding.
cookies = {} # what the browser sends back
def visit(cookies):
visits = ___(cookies.get("visits", "0")) + 1 # 👉 replace ___ with int
cookies["visits"] = str(visits) # like resp.set_cookie("visits", ...)
return visits
print("visits:", visit(cookies))
print("visits:", visit(cookies))
# 3) UPLOADS: only accept safe extensions, like the allowed_file() helper.
ALLOWED = {"png", "jpg", "pdf"}
def allowed_file(filename):
return "." in filename and filename.rsplit(".", 1)[1].lower() in ___ # 👉 replace ___ with ALLOWED
for name in ["report.pdf", "photo.JPG", "virus.exe"]:
print(name, "->", "accepted" if allowed_file(name) else "rejected")
# ✅ Expected output:
# /
# /tasks/7
# visits: 1
# visits: 2
# report.pdf -> accepted
# photo.JPG -> accepted
# virus.exe -> rejected📝 Checkpoint Quiz
Try to answer each question in your head first, then expand it to check. These cover the load-bearing ideas from the whole section.
A: url_for. You pass the endpoint name (the view function's name) plus any route parameters, and Flask returns the correct URL. Because nothing is hard-coded, changing a route's path later doesn't break your links.
A: Build a response with make_response(...), then call response.set_cookie("name", "value"). You can't set a cookie on a plain string return — you need a real response object. The browser sends the cookie back on later requests, where you read it with request.cookies.get(...).
A: It must return the response object (usually the same one it received, after modifying it). If you forget the return, the hook replaces the response with None and Flask raises an error.
A: The get(self) method. A MethodView dispatches each HTTP method to the same-named method on the class — POST to post, PUT to put, and so on — which keeps a resource's logic grouped instead of branching on request.method.
A: Debug mode shows the interactive traceback (the Werkzeug debugger), which can expose source code, configuration, and even let a visitor run arbitrary code on your server. It also auto-reloads on file changes. Both are fine locally but dangerous in production, so set DEBUG = False there.
A: g is a per-request scratchpad for data you want to share across functions during a single request — a database connection, the current user, or a request id. It's created fresh for every request and discarded when the request ends, so a value set in a before_request hook is readable in the view and the after_request hook.
Watch Out For These
❌ Forgetting to return the response from after_request
The hook must return response. Without it, the response becomes None and Flask errors out. This is the single most common mistake when wiring up request hooks.
❌ Treating a cookie value as a number
Cookies are always strings. Reading request.cookies.get("visits", "0") and adding 1 directly would concatenate, so convert with int(...) first.
❓ Frequently Asked Questions
Checkpoint complete — you can build a real dynamic app!
You combined templates, a cookie, request hooks with g, and a MethodView into one working page — the same shape that powers production Flask apps. Everything from this section now fits together in your hands.
Practice quiz
Which Flask function builds URLs to your routes instead of hard-coding paths?
- redirect
- make_response
- url_for
- render_template_string
Answer: url_for. url_for builds URLs from the endpoint name plus route parameters, so changing a route's path later does not break your links.
How do you set a cookie on a response in Flask?
- Build a response with make_response(...) and call response.set_cookie(...)
- return a plain string with the cookie appended
- Assign to request.cookies directly
- Call session.set_cookie(...)
Answer: Build a response with make_response(...) and call response.set_cookie(...). You need a real response object: make_response(...) then response.set_cookie('name', 'value'); you cannot set a cookie on a plain string return.
What must an after_request handler return?
- None
- A status code integer
- The request object
- The response object it received (after modifying it)
Answer: The response object it received (after modifying it). An after_request handler must return the response object; forgetting the return replaces it with None and Flask raises an error.
When a GET request hits a MethodView, which method runs?
- handle()
- get(self)
- dispatch(self)
- index(self)
Answer: get(self). A MethodView dispatches each HTTP method to a same-named method, so a GET calls get(self), a POST calls post(self), and so on.
Why must DEBUG be False in production?
- The interactive debugger can expose source/config and allow arbitrary code execution
- It speeds up template rendering
- It enables cookies
- It is required by url_for
Answer: The interactive debugger can expose source/config and allow arbitrary code execution. Debug mode shows the Werkzeug interactive traceback, which can leak source and config and even let a visitor run arbitrary code on the server.
What is the g object used for in Flask?
- Global config shared across all requests forever
- A cookie storage helper
- A per-request scratchpad for data shared across functions during one request
- A template rendering engine
Answer: A per-request scratchpad for data shared across functions during one request. g is a per-request scratchpad created fresh each request, so a value set in before_request is readable in the view and after_request.
Since cookies are always strings, how do you safely use a visit counter cookie as a number?
- Add 1 to it directly
- Convert it with int(...) before adding
- Store it as bytes
- Use float() on the response
Answer: Convert it with int(...) before adding. Cookies are strings, so int(request.cookies.get('visits', '0')) + 1 converts before adding; adding 1 directly would concatenate.
Which Jinja2 construct loops over a list of tasks inside a template?
- {{ for t in tasks }}
- <for each='tasks'>
- @for tasks
- {% for t in tasks %} ... {% endfor %}
Answer: {% for t in tasks %} ... {% endfor %}. Jinja2 uses statement tags {% for t in tasks %} ... {% endfor %} to loop, while {{ }} is for outputting expressions.
How do you register a MethodView subclass on a URL?
- @app.route('/') above the class
- app.add_url_rule('/', view_func=HomeView.as_view('home'))
- app.register(HomeView)
- HomeView.route('/')
Answer: app.add_url_rule('/', view_func=HomeView.as_view('home')). You call as_view('endpoint') to turn the class into a view function and pass it to app.add_url_rule(path, view_func=...).
In the request lifecycle, when does the before_request hook run relative to the view?
- After the view returns
- Only on POST requests
- Before every view, so g can be set for the view to read
- Never, unless registered per route
Answer: Before every view, so g can be set for the view to read. before_request runs before every view, so an id stashed on g there is available to the view and to the after_request hook.
Continue this course
- Previous: Class-Based Views (MethodView)
- Next: User Login with Flask-Login — Add a login system with sessions, @login_required, and current_user
- Quick reference: Flask cheat sheet
Frequently asked questions
Which function builds URLs to my routes in Flask?
url_for builds URLs by endpoint name rather than hard-coded paths. You pass the view function's name (and any route parameters) and Flask returns the correct URL — so when you change a route's path later, every link still works because nothing was hard-coded. It also handles static files via url_for('static', filename='...').
How do I set a cookie, and how do I read it back?
Cookies attach to a response, so build one with make_response(...) and call response.set_cookie('name', 'value'). The browser stores it and sends it back on later requests, where you read it with request.cookies.get('name', default). Cookies are strings, so convert numbers with int(...) when you read them, as a visit counter does.
What does an after_request handler have to return?
An after_request handler must return the response object — usually the same one it received after modifying it (for example, adding a header). If you forget the return, Flask raises an error because the hook replaced the response with None. Use after_request to add headers, set cookies globally, or log every response on its way out.
How does a MethodView dispatch an incoming request?
A MethodView routes each HTTP method to a same-named method on the class: a GET request calls get(self), a POST calls post(self), PUT calls put(self), and so on. You register it with app.add_url_rule('/path', view_func=MyView.as_view('endpoint')). This keeps the logic for one resource grouped together instead of branching on request.method inside a single function.