When a Flask view needs to return more than a string or a dict — for example a 201 status with a Location header, or a cookie attached to the response — use make_response(). It wraps the content into a Response object so you can adjust the status, headers, and cookies before returning, instead of settling for the default 200 status.
The Problem
By default, Flask wraps whatever a view returns into a 200 response. That is enough for endpoints that only return data, but it falls short when you need something attached. The user-creation endpoint below returns the right data, yet the client never learns where the new resource lives, nor does it receive a header to trace logs with:
from flask import Flask, jsonify, requestapp = Flask(__name__)@app.post("/users")def create_user(): data = request.get_json(silent=True) or {} if not data.get("name"): return jsonify({"error": "Missing field name"}), 400 # Returns the right data, but still 200, with no Location header return jsonify({"id": 42, "name": data["name"]})
Returning a (body, status, headers) tuple handles simple statuses and headers, but when you also need to set a cookie, you would have to hand-write the Set-Cookie header yourself — easy to get wrong in format and hard to read when one response needs a status, headers, and a cookie all at once.
The Solution
make_response() takes the return value and gives back a Response object you can edit before returning. For a newly created resource, set status 201 and a Location header pointing at it, plus a custom header for log tracing:
from flask import Flask, jsonify, make_response, requestapp = Flask(__name__)@app.post("/users")def create_user(): data = request.get_json(silent=True) or {} if not data.get("name"): return jsonify({"error": "Missing field name"}), 400 response = make_response(jsonify({"id": 42, "name": data["name"]})) response.status_code = 201 response.headers["Location"] = "/api/users/42" response.headers["X-Request-Id"] = "abc-123" return response
For cookies, set_cookie() and delete_cookie() do exactly the hard part: header formatting, expiry, and security flags. Do not hand-concatenate a Set-Cookie string when these methods already exist:
@app.post("/theme")def set_theme(): response = make_response(jsonify({"theme": "dark"})) response.set_cookie("theme", "dark", httponly=True, samesite="Lax", max_age=30 * 24 * 3600) return response@app.post("/theme/logout")def clear_theme(): response = make_response(jsonify({"theme": None})) response.delete_cookie("theme") return response
Note: delete_cookie() does not delete the cookie immediately; it sends the cookie back with an expiry time in the past so the browser drops it on its own. If an endpoint only needs a different status, the (body, status) tuple is still concise enough — do not overuse make_response() for every view.
Apply It to Your Project
Review your endpoints and apply make_response() where it belongs: resource-creation endpoints return 201 with Location, login/logout endpoints set or clear cookies, and responses that need an X-Request-Id header for matching logs when incidents happen. Before shipping to production, verify with Flask’s test client — assert the status, headers, and cookie, not just the body:
client = app.test_client()resp = client.post("/users", json={"name": "Linh"})assert resp.status_code == 201assert resp.headers["Location"] == "/api/users/42"resp = client.post("/theme")assert "theme=dark" in resp.headers["Set-Cookie"]
This habit catches “right data, wrong status, missing header” responses early — the kind of bug that is hard to spot from the browser alone.
Related Posts
- Flask: Stop Circular Imports — Use current_app Instead of Importing app Directly in 5 Minutes
- Flask: Stop Hardcoding URLs — Use url_for So Route Changes Never Break Links
- Flask: Unstick Your Session After a Failed Save with db.session.rollback() in 5 Minutes
Frequently Asked Questions
How is make_response() different from returning a (body, status, headers) tuple?
A tuple covers simple statuses and headers, but cannot set cookies. make_response() returns a full Response object, so you get set_cookie(), delete_cookie(), and the ability to modify headers repeatedly.
When do I not need make_response()?
When a view only returns data with status 200, or only needs a different status, the (body, status) tuple is enough. Use make_response() when the response needs custom headers, cookies, or all three at once.
How is set_cookie() different from Flask’s session?
set_cookie() writes a raw cookie that you read back yourself via request.cookies. Flask’s session also lives in a cookie, but it is signed with SECRET_KEY and decoded back into a dict automatically. Never store sensitive data in an unsigned raw cookie.
Why does the browser still send the old cookie after delete_cookie()?
Because delete_cookie() only sends an expired cookie back to the browser; the browser removes it only after receiving that response. If you check before the response arrives, the old cookie is still there.
What happens when I overwrite an existing header?
Assigning response.headers["X-Request-Id"] = ... replaces the old value. To keep several values for the same header, use response.headers.add() instead of direct assignment.
💬 Have you tried this in your own project? If you hit an error or have a cleaner way, leave a comment below — I read them all and will fold the best ideas into the post.
References
- Flask documentation, “About Responses” — https://flask.palletsprojects.com/en/stable/quickstart/#about-responses
- Flask documentation,
flask.make_response— https://flask.palletsprojects.com/en/stable/api/#flask.make_response
