JPG
JPG

Reputation: 88619

FastAPI: Retrieve URL from view name ( route name )

Suppose I have following views,

from fastapi import FastAPI

app = FastAPI()


@app.get('/hello/')
def hello_world():
    return {"msg": "Hello World"}


@app.get('/hello/{number}/')
def hello_world_number(number: int):
    return {"msg": "Hello World Number", "number": number}

I have been using these functions in Flask and Django

So, how can I obtain/build the URLs of hello_world and hello_world_number in a similar way?

Upvotes: 39

Views: 52272

Answers (5)

Christian Sauer
Christian Sauer

Reputation: 10909

url_for exists, but is provided by starlette, the server underpinning FastAPI:

https://www.starlette.io/routing/#reverse-url-lookups

Upvotes: 3

JPG
JPG

Reputation: 88619

We have got Router.url_path_for(...) method which is located inside the starlette package

Method-1: Using FastAPI instance

This method is useful when you are able to access the FastAPI instance in your current context. (Thanks to @Yagizcan Degirmenci)

from fastapi import FastAPI

app = FastAPI()


@app.get('/hello/')
def hello_world():
    return {"msg": "Hello World"}


@app.get('/hello/{number}/')
def hello_world_number(number: int):
    return {"msg": "Hello World Number", "number": number}


print(app.url_path_for('hello_world'))
print(app.url_path_for('hello_world_number', number=1))
print(app.url_path_for('hello_world_number', number=2))

# Results

"/hello/"
"/hello/1/"
"/hello/2/"

Drawback

  • If we are using APIRouter, router.url_path_for('hello_world') may not work since router isn't an instance of FastAPI class. That is, we must have the FastAPI instance to resolve the URL

Method-2: Request instance

This method is useful when you are able to access the Request instance (the incoming request), usually, within a view.

from fastapi import FastAPI, Request

app = FastAPI()


@app.get('/hello/')
def hello_world():
    return {"msg": "Hello World"}


@app.get('/hello/{number}/')
def hello_world_number(number: int):
    return {"msg": "Hello World Number", "number": number}


@app.get('/')
def named_url_reveres(request: Request):
    return {
        "URL for 'hello_world'": request.url_for("hello_world"),
        "URL for 'hello_world_number' with number '1'": request.url_for("hello_world_number", number=1),
        "URL for 'hello_world_number' with number '2''": request.url_for("hello_world_number", number=2})
    }

# Result Response

{
    "URL for 'hello_world'": "http://0.0.0.0:6022/hello/",
    "URL for 'hello_world_number' with number '1'": "http://0.0.0.0:6022/hello/1/",
    "URL for 'hello_world_number' with number '2''": "http://0.0.0.0:6022/hello/2/"
}

Drawback

  • We must include the request parameter in every (or required) view to resolve the URL, which might raise an ugly feel to developers.

Upvotes: 63

stephane
stephane

Reputation: 31

If you need to resolve URLs in template, Starlette (so FastAPI) defines the url_for() function in the default Jinja environment by using the request instance of the context and url_for method:

https://github.com/encode/starlette/blob/master/starlette/templating.py#L63

To use it in template: {{ url_for('hello_world_number', number=42) }}.

Upvotes: 0

liber
liber

Reputation: 196

If the same function name is defined under multiple APIRouters, request.url_for and router.url_path_for would return the first matching function name (in the order of include_router).
Here is a way to get the correct url with the tag of APIRouter when there is a function name conflict, if someone needs it:
Step 1: put this in your __init__.py:

def url_of(request: Request, name: str, **path_params: dict):
    from fastapi.routing import APIRoute
    from starlette.routing import NoMatchFound
    tag, tid, fname = None, name.find('.'), name
    if tid > 0:
        tag = name[:tid]
        fname = name[tid + 1:]
    url_no_tag = None
    for route in request.app.router.routes:
        if not isinstance(route, APIRoute):
            continue
        if fname == route.name and (not tag or tag in route.tags):
            try:
                url_path = route.url_path_for(fname, **path_params)
                url_no_tag = url_path.make_absolute_url(base_url=request.base_url)
                if tag:
                    return url_no_tag
            except NoMatchFound:
                pass
    if url_no_tag:
        return url_no_tag
    return request.url_for(name, **path_params)

Step 2: add a tag for APIRouters:

router = APIRouter(prefix='/user', tags=['user'])
@router.get('/')
def login():
    return 'login page'

Step 3: retrieve the url in any where:

@router2.get('/test')
def test(request: Request):
    return RedirectResponse(url_of(request, 'user.login') + '?a=1')

2021/07/10 rename url_as to url_of

Upvotes: 1

Yagiz Degirmenci
Yagiz Degirmenci

Reputation: 20756

Actually you don't need to reinvent the wheel. FastAPI supports this out-of-box (Actually Starlette), and it works pretty well.

app = FastAPI()

@app.get("/hello/{number}/")
def hello_world_number(number: int):
    return {"msg": "Hello World Number", "number": number}

If you have an endpoint like this you can simply use

In:  app.url_path_for("hello_world_number", number=3)
In:  app.url_path_for("hello_world_number", number=50)

Out: /hello/3/
Out: /hello/50/

In FastAPI, APIRouter and FastAPI(APIRoute) inherits from Router(Starlette's) so, if you have an APIRouter like this, you can keep using this feature

router = APIRouter()

@router.get("/hello")
def hello_world():
    return {"msg": "Hello World"}

In:  router.url_path_for("hello_world")
Out: /hello

Upvotes: 13

Related Questions