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, request
app = 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, request
app = 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 == 201
assert 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

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