mirror of
https://github.com/kennethreitz/responder.git
synced 2026-10-24 05:59:12 +00:00
272 lines
8.1 KiB
ReStructuredText
272 lines
8.1 KiB
ReStructuredText
Migrating from Flask
|
|
====================
|
|
|
|
If you're coming from Flask, you'll find Responder familiar but different
|
|
in a few key ways. This guide maps Flask concepts to their Responder
|
|
equivalents and shows you how to translate common patterns.
|
|
|
|
|
|
The Big Differences
|
|
-------------------
|
|
|
|
**Mutate the response, or return one.** In Flask you return a response.
|
|
Responder hands you a ``resp`` object to mutate — that's the idiomatic style,
|
|
and it keeps the response editable through hooks and middleware:
|
|
|
|
Flask::
|
|
|
|
@app.route("/")
|
|
def hello():
|
|
return "hello, world!"
|
|
|
|
Responder::
|
|
|
|
@api.route("/")
|
|
def hello(req, resp):
|
|
resp.text = "hello, world!"
|
|
|
|
Flask-style returns work too. A string becomes the body; a dictionary, list,
|
|
number, or boolean becomes JSON; a Pydantic model or dataclass is serialized
|
|
for you; and a ``(body, status[, headers])`` tuple sets the lot at once::
|
|
|
|
@api.route("/")
|
|
def hello(req, resp):
|
|
return "hello, world!"
|
|
|
|
@api.route("/items", methods=["POST"])
|
|
async def create(req, resp):
|
|
return {"created": True}, 201
|
|
|
|
See :ref:`accepted return types <returning-values>` for the complete list.
|
|
|
|
**Explicit request and response.** Flask uses a global ``request`` object
|
|
(via thread-local magic). Responder passes ``req`` and ``resp`` explicitly.
|
|
No magic, no import needed — they're right there in the function signature.
|
|
|
|
**ASGI, not WSGI.** Flask runs on WSGI, which is synchronous. Responder
|
|
runs on ASGI, which supports async natively. You can still write sync
|
|
views — Responder runs them in a thread pool automatically.
|
|
|
|
|
|
Quick Reference
|
|
---------------
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 40 60
|
|
|
|
* - Flask
|
|
- Responder
|
|
* - ``Flask(__name__)``
|
|
- ``responder.API()``
|
|
* - ``return "text"``
|
|
- ``resp.text = "text"``
|
|
* - ``return jsonify(data)``
|
|
- ``resp.media = data``
|
|
* - ``return render_template("t.html", x=1)``
|
|
- ``resp.html = api.template("t.html", x=1)``
|
|
* - ``request.args["q"]``
|
|
- ``req.params["q"]``
|
|
* - ``request.json``
|
|
- ``await req.media()``
|
|
* - ``request.form``
|
|
- ``await req.media("form")``
|
|
* - ``request.headers["X"]``
|
|
- ``req.headers["X"]``
|
|
* - ``request.method``
|
|
- ``req.method``
|
|
* - ``request.cookies["x"]``
|
|
- ``req.cookies["x"]``
|
|
* - ``session["x"] = 1``
|
|
- ``resp.session["x"] = 1``
|
|
* - ``abort(404)``
|
|
- ``responder.abort(404)``
|
|
* - ``redirect("/new")``
|
|
- ``api.redirect(resp, location="/new")``
|
|
* - ``@app.before_request``
|
|
- ``@api.before_request``
|
|
* - ``@app.errorhandler(404)``
|
|
- ``@api.exception_handler(404)``
|
|
* - ``app.run(debug=True)``
|
|
- ``api.run(debug=True)``
|
|
|
|
|
|
A few of these mappings deserve a closer look:
|
|
|
|
- ``responder.abort(404)`` *raises* and halts the handler, exactly like
|
|
Flask's ``abort()`` — assigning ``resp.status_code`` alone does not stop
|
|
execution. Import it from ``responder``; it works from handlers, hooks, and
|
|
dependencies alike.
|
|
- ``@api.exception_handler`` takes an exception class *or* a status code, so
|
|
``@api.exception_handler(404)`` mirrors ``@app.errorhandler(404)``. The
|
|
handler receives ``(req, resp, exc)``.
|
|
- **Sessions need a secret key.** ``resp.session`` and ``req.session`` ride on
|
|
a signed cookie, so Responder needs a signing key: set ``API(secret_key=...)``
|
|
or the ``RESPONDER_SECRET_KEY`` environment variable. Without one, a random
|
|
per-process key is minted (with a loud warning), so sessions won't survive a
|
|
restart or span multiple workers; ``secret_key="NOTASECRET"`` is rejected, and
|
|
cookies are marked ``Secure`` in production. See :doc:`guide-config`.
|
|
- **Redirects can block open redirects.** Pass ``allow_external=False`` to
|
|
``api.redirect`` when the location comes from user input (e.g. a ``?next=``
|
|
parameter).
|
|
|
|
|
|
Route Parameters
|
|
----------------
|
|
|
|
Flask uses ``<angle_brackets>``. Responder uses ``{curly_braces}``
|
|
with the same type convertor idea:
|
|
|
|
Flask::
|
|
|
|
@app.route("/users/<int:user_id>")
|
|
def get_user(user_id):
|
|
return jsonify({"id": user_id})
|
|
|
|
Responder::
|
|
|
|
@api.route("/users/{user_id:int}")
|
|
def get_user(req, resp, *, user_id):
|
|
resp.media = {"id": user_id}
|
|
|
|
Note the ``*`` — route parameters are keyword-only arguments in
|
|
Responder. This makes the interface explicit about which arguments
|
|
come from the URL.
|
|
|
|
Query-string parameters can be typed the same way. Instead of reaching into
|
|
``req.params``, declare them with the ``Query`` marker and Responder coerces
|
|
the value — returning ``422`` if it's missing or the wrong type::
|
|
|
|
from responder import Query
|
|
|
|
@api.route("/search")
|
|
def search(req, resp, *, q: str = Query(...), limit: int = Query(10)):
|
|
resp.media = {"q": q, "limit": limit}
|
|
|
|
``Query(...)`` is required; ``Query(10)`` supplies a default. There are
|
|
matching ``Header``, ``Cookie``, and ``Path`` markers too.
|
|
|
|
|
|
JSON APIs
|
|
---------
|
|
|
|
Flask::
|
|
|
|
@app.route("/api/items", methods=["POST"])
|
|
def create_item():
|
|
data = request.json
|
|
# ... create item
|
|
return jsonify(item), 201
|
|
|
|
Responder::
|
|
|
|
@api.route("/api/items", methods=["POST"])
|
|
async def create_item(req, resp):
|
|
data = await req.media()
|
|
# ... create item
|
|
return item, 201
|
|
|
|
The ``await`` is needed because reading the request body is async I/O — the
|
|
event loop stays free while the body arrives. The ``return item, 201`` is the
|
|
Flask-style tuple from earlier; you could just as well mutate ``resp.media``
|
|
and ``resp.status_code``.
|
|
|
|
**Skip the manual parsing.** Flask hands you ``request.json`` and leaves
|
|
validation to you. Responder can do better: annotate a keyword-only parameter
|
|
with a Pydantic model and the body is parsed, validated, and injected for you —
|
|
with an automatic ``422`` (and the validation errors) when it doesn't fit::
|
|
|
|
from pydantic import BaseModel
|
|
|
|
class ItemIn(BaseModel):
|
|
name: str
|
|
price: float
|
|
|
|
@api.route("/api/items", methods=["POST"])
|
|
async def create_item(req, resp, *, item: ItemIn):
|
|
# `item` is a validated ItemIn — no try/except needed
|
|
return {"id": 1, "name": item.name}, 201
|
|
|
|
The same idea runs in reverse: a Pydantic *return* annotation becomes the
|
|
response model, validating and trimming the payload on the way out. See
|
|
:doc:`tutorial-rest` for the full typed-I/O walkthrough.
|
|
|
|
|
|
Templates
|
|
---------
|
|
|
|
Both use Jinja2. The syntax is nearly identical:
|
|
|
|
Flask::
|
|
|
|
@app.route("/hello/<name>")
|
|
def hello(name):
|
|
return render_template("hello.html", name=name)
|
|
|
|
Responder::
|
|
|
|
@api.route("/hello/{name}")
|
|
def hello(req, resp, *, name):
|
|
resp.html = api.template("hello.html", name=name)
|
|
|
|
|
|
Blueprints → Routers
|
|
--------------------
|
|
|
|
Flask uses Blueprints to organize routes across modules. Responder's
|
|
standalone :class:`responder.Router` works the same way — declare routes in
|
|
a module without an app instance, then register the router on the app:
|
|
|
|
Flask::
|
|
|
|
bp = Blueprint("api", __name__, url_prefix="/api")
|
|
|
|
@bp.route("/users")
|
|
def list_users():
|
|
return jsonify([])
|
|
|
|
app.register_blueprint(bp)
|
|
|
|
Responder::
|
|
|
|
from responder import Router
|
|
|
|
router = Router(prefix="/api")
|
|
|
|
@router.route("/users")
|
|
def list_users(req, resp):
|
|
resp.media = []
|
|
|
|
api.include_router(router)
|
|
|
|
Routers nest, take an extra prefix at inclusion time, and carry group-level
|
|
``tags``, ``dependencies``, and ``auth`` — see :doc:`routers`. For quick
|
|
same-file prefix grouping there is also ``api.group("/api")``, which
|
|
registers routes immediately on the live ``api``.
|
|
|
|
|
|
Gradual Migration
|
|
-----------------
|
|
|
|
You don't have to migrate all at once. Responder can mount your existing
|
|
Flask app at a subroute, so you can move endpoints over one at a time::
|
|
|
|
from flask import Flask
|
|
|
|
flask_app = Flask(__name__)
|
|
|
|
# Your existing Flask routes stay here
|
|
@flask_app.route("/legacy")
|
|
def legacy():
|
|
return "old endpoint"
|
|
|
|
# Mount Flask under /old, new routes go on Responder
|
|
api.mount("/old", flask_app)
|
|
|
|
@api.route("/new")
|
|
def new_endpoint(req, resp):
|
|
resp.media = {"modern": True}
|
|
|
|
Requests to ``/old/legacy`` go to Flask. Everything else goes to
|
|
Responder. When you've moved everything over, remove the mount.
|