Python Flask — Written Test
Prep
How this section usually works
You're typically asked to build or extend a small
REST API — define routes, handle requests, talk
to a database via an ORM, and handle errors
properly. Graders check:
1. Correct route definitions and HTTP
methods/status codes
2. Proper use of the request object (JSON body,
query params, path params)
3. Database interaction via SQLAlchemy done
correctly (sessions, relationships, avoiding
N+1 queries)
4. Sensible error handling (not letting a stack
trace leak to the client)
5. Basic security awareness (input validation,
not trusting client input blindly)
1. Basic app structure and routing
from flask import Flask, request,
jsonify
app = Flask(__name__)
@[Link]("/")
def home():
return "Hello, Flask!"
@[Link]("/users/<int:user_id>") #
converter enforces int, auto-404s on
non-numeric
def get_user(user_id):
return jsonify({"id": user_id,
"name": "Sample User"})
@[Link]("/users", methods=["POST"])
def create_user():
data = request.get_json()
return jsonify(data), 201
if __name__ == "__main__":
[Link](debug=True)
Detail worth stating: using <int:user_id> (a
route converter) instead of <user_id> means
Flask itself rejects non-numeric values with a
404 before your function body even runs — this is
cheaper and more consistent than a manual
try: int(user_id) check inside every handler.
debug=True should never be used in production
— it exposes an interactive debugger that can
execute arbitrary code if reached.
2. Request handling — query
params, form data, JSON,
headers
@[Link]("/search")
def search():
query = [Link]("q", "")
# query string: /search?q=phone
page = [Link]("page", 1,
type=int) # with default + type
coercion
return jsonify({"query": query,
"page": page})
@[Link]("/submit", methods=["POST"])
def submit():
if request.is_json:
data = request.get_json()
else:
data = [Link].to_dict()
# standard form POST (not JSON)
return jsonify(received=data)
@[Link]("/protected")
def protected():
auth_header =
[Link]("Authorization")
if not auth_header:
return jsonify(error="Missing
Authorization header"), 401
return jsonify(message="Access
granted")
Q: What's the difference between
[Link] , [Link] , and
request.get_json() ?
[Link] reads query string parameters
( ?key=value in the URL) — always present
regardless of method. [Link] reads
multipart/form-data or application/x-
www-form-urlencoded bodies (classic HTML
form submissions). request.get_json()
parses a JSON body, and returns None (or
raises, depending on force / silent flags) if
the Content-Type header isn't
application/json — a common gotcha
when a client forgets to set that header.
3. Dynamic routes, Blueprints
(structuring a larger app)
# blueprints/[Link]
from flask import Blueprint, jsonify
users_bp = Blueprint("users", __name__,
url_prefix="/users")
@users_bp.route("/<int:user_id>")
def get_user(user_id):
return jsonify({"id": user_id})
# [Link]
from flask import Flask
from [Link] import users_bp
app = Flask(__name__)
app.register_blueprint(users_bp)
Why Blueprints matter (a common "how would
you structure this" question): as an app grows
past a handful of routes, keeping everything in
one [Link] becomes unmanageable. Blueprints
let you group related routes (users, orders, auth)
into separate modules, each independently
definable and testable, then wire them together
in the main app file with a single
register_blueprint call — this is the standard
answer to "how do you organize a larger Flask
application."
4. Flask-SQLAlchemy — models,
relationships, queries
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
class Department([Link]):
id = [Link]([Link],
primary_key=True)
name = [Link]([Link](100),
nullable=False)
employees =
[Link]("Employee",
backref="department", lazy=True)
class Employee([Link]):
id = [Link]([Link],
primary_key=True)
name = [Link]([Link](100),
nullable=False)
salary = [Link]([Link],
nullable=False)
department_id =
[Link]([Link],
[Link]("[Link]"),
nullable=False)
Q: Write an endpoint that returns all employees
in a department, using the relationship.
@[Link]("/departments/<int:dept_id>/
employees")
def department_employees(dept_id):
department =
[Link].get_or_404(dept_id)
return jsonify([
{"id": [Link], "name": [Link],
"salary": [Link]}
for e in [Link]
])
get_or_404 is worth using explicitly — it's a one-
liner that replaces a manual "fetch, check if
None, return 404" block, and is exactly the kind of
Flask-idiomatic detail graders look for.
Q: What's the N+1 query problem, and how
would you avoid it here?
If you queried all departments and then
accessed .employees on each one in a loop,
SQLAlchemy would (by default, with
lazy=True ) issue one query to fetch
departments, then one additional query per
department to fetch its employees — N+1
total queries instead of 2. The fix is eager
loading:
from [Link] import joinedload
departments =
[Link](joinedload(Dep
[Link])).all()
This makes SQLAlchemy fetch departments and
their employees in a single JOIN query. Naming
this problem and its fix unprompted is a strong
signal on an ORM-focused question.
Q: Create a new employee (POST), with basic
validation.
@[Link]("/employees", methods=
["POST"])
def create_employee():
data = request.get_json()
if not data or "name" not in data
or "salary" not in data or
"department_id" not in data:
return jsonify(error="Missing
required fields"), 400
department =
[Link](data["department_i
d"])
if not department:
return
jsonify(error="Department not found"),
404
employee = Employee(
name=data["name"],
salary=data["salary"],
department_id=data["department_id"]
)
[Link](employee)
[Link]()
return jsonify({"id": [Link],
"name": [Link]}), 201
Detail to mention: validate that referenced
foreign keys ( department_id ) actually exist
before inserting — otherwise you either get a raw
database integrity error leaking to the client, or
(on a misconfigured DB without FK constraints)
silently create an orphaned row.
Q: Update and delete an employee.
@[Link]("/employees/<int:emp_id>",
methods=["PUT"])
def update_employee(emp_id):
employee =
[Link].get_or_404(emp_id)
data = request.get_json()
[Link] = [Link]("name",
[Link])
[Link] =
[Link]("salary", [Link])
[Link]()
return jsonify({"id": [Link],
"name": [Link], "salary":
[Link]})
@[Link]("/employees/<int:emp_id>",
methods=["DELETE"])
def delete_employee(emp_id):
employee =
[Link].get_or_404(emp_id)
[Link](employee)
[Link]()
return "", 204 # 204 No Content —
successful, nothing to return
Returning 204 with an empty body for a
successful delete (rather than 200 with a
message) is the REST convention worth stating
explicitly.
5. Error handling
@[Link](404)
def not_found(e):
return jsonify(error="Resource not
found"), 404
@[Link](500)
def server_error(e):
return jsonify(error="Internal
server error"), 500
# Custom exception + handler
class
InsufficientStockError(Exception):
pass
@[Link](InsufficientStockErro
r)
def handle_stock_error(e):
return jsonify(error=str(e)), 400
@[Link]("/orders", methods=["POST"])
def create_order():
data = request.get_json()
if [Link]("quantity", 0) >
get_available_stock(data["product_id"])
:
raise
InsufficientStockError("Not enough
stock available")
# ... proceed with order creation
Why custom exception classes matter: they let
you raise a domain-specific error deep inside
business logic (a service function several layers
down) without that function needing to know
anything about HTTP status codes — the
@[Link] at the top level is the only
place that translates it into an actual HTTP
response. This keeps business logic decoupled
from the web layer, which is a detail worth
stating if asked "how would you structure error
handling in a larger app."
6. Middleware-style hooks:
before_request / after_request
import time
@app.before_request
def start_timer():
request.start_time = [Link]()
@app.after_request
def log_request(response):
duration = [Link]() -
request.start_time
[Link](f"{[Link]}
{[Link]} took {duration:.3f}s")
return response
Use case to mention: before_request is also
where you'd typically implement things that need
to apply to every route without repeating code in
each handler — e.g. checking an API key header,
rejecting requests over a certain size, or opening
a DB connection/session per request.
7. Authentication (JWT-based,
the common pattern)
import jwt
import datetime
from functools import wraps
SECRET_KEY = "your-secret-key" # in
real code: load from environment, never
hardcode
def generate_token(user_id):
payload = {
"user_id": user_id,
"exp":
[Link]() +
[Link](hours=1)
}
return [Link](payload,
SECRET_KEY, algorithm="HS256")
def token_required(f):
@wraps(f)
def decorated(*args, **kwargs):
token =
[Link]("Authorization",
"").replace("Bearer ", "")
if not token:
return jsonify(error="Token
missing"), 401
try:
payload = [Link](token,
SECRET_KEY, algorithms=["HS256"])
request.user_id =
payload["user_id"]
except
[Link]:
return jsonify(error="Token
expired"), 401
except [Link]:
return
jsonify(error="Invalid token"), 401
return f(*args, **kwargs)
return decorated
@[Link]("/profile")
@token_required
def profile():
return jsonify({"user_id":
request.user_id})
Explain the decorator pattern: token_required
wraps any route function so the token-validation
logic is written once and reused via
@token_required , rather than copy-pasted into
every protected route. @wraps(f) preserves the
original function's name/metadata (without it,
Flask's routing can break when multiple routes
are wrapped by the same decorator, since they'd
all appear to be named decorated ).
Distinguishing ExpiredSignatureError from a
generally invalid token is a detail worth including
— it lets the client tell "log in again" apart from
"something is actually wrong with this token."
8. CORS
from flask_cors import CORS
app = Flask(__name__)
CORS(app, resources={r"/api/*":
{"origins": "[Link]
Q: What is CORS and why does it matter for a
Flask API serving a separate frontend?
CORS (Cross-Origin Resource Sharing) is a
browser security mechanism that blocks
JavaScript on one origin
(domain+port+protocol) from reading
responses from a different origin, unless the
server explicitly allows it via response
headers. If your React frontend runs on
localhost:3000 and your Flask API on
localhost:5000 , the browser blocks the
frontend's fetch calls by default — flask-
cors adds the necessary Access-Control-
Allow-Origin headers. Scoping it to specific
origins/routes (rather than a blanket
origins: "*" ) is worth mentioning as the
more secure default for anything beyond local
development.
9. Testing with pytest and Flask's
test client
import pytest
from app import app, db
@[Link]
def client():
[Link]["TESTING"] = True
[Link]["SQLALCHEMY_DATABASE_URI"] =
"sqlite:///:memory:"
with app.test_client() as client:
with app.app_context():
db.create_all()
yield client
def test_create_employee(client):
response =
[Link]("/employees", json={
"name": "Jane Doe", "salary":
75000, "department_id": 1
})
assert response.status_code in
(201, 404) # 404 if dept doesn't exist
in this test DB
def
test_get_nonexistent_employee(client):
response =
[Link]("/employees/9999")
assert response.status_code == 404
Detail worth mentioning: using an in-memory
SQLite database ( sqlite:///:memory: ) for
tests keeps them fast and fully isolated from
your real database — each test run starts from a
clean slate via db.create_all() , so tests never
leave residue that could make later runs
pass/fail inconsistently.
10. Application context & request
context (a common "gotcha"
question)
Q: Why does code outside a route sometimes
throw "working outside of application context"?
Flask uses two context stacks: the application
context (holds things like current_app , the
DB connection config) and the request
context (holds request , session — only
exists during an actual HTTP request). Code
that touches db or current_app outside of a
route function, a CLI command, or an explicit
with app.app_context(): block has no
active context to attach to, and Flask raises
this error rather than silently guessing which
app/config to use. This commonly bites
people writing a standalone script that
imports models and queries the DB directly —
the fix is wrapping that script's logic in with
app.app_context(): .
General template for any Flask
question
1. Identify the route: path, HTTP method,
path/query params needed.
2. Validate input early — required fields present,
correct types, referenced foreign keys exist —
before touching the database.
3. Use the ORM relationship/eager-loading
correctly — watch for N+1 queries when a
question involves nested/related data.
4. Return the correct status code: 200 OK, 201
Created, 204 No Content (delete), 400 bad
input, 401 unauthorized, 404 not found,
500 only for truly unexpected server errors
(never leak a raw traceback to the client).
5. Mention security/structure concerns
unprompted where relevant — hardcoded
secrets, missing auth checks, N+1 queries, or
a monolithic [Link] that should be split into
Blueprints — these are exactly the "senior"
details a written test is trying to surface.