4

(I did find the following question on SO, but it didn't help me: Is it possible to have an api call another api, having them both in same application?)

I am making an app using Fastapi with the following folder structure

enter image description here

main.py is the entry point to the app

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.api.v1 import lines, upload
from app.core.config import settings

app = FastAPI(
    title=settings.PROJECT_NAME,
    version=0.1,
    openapi_url=f'{settings.API_V1_STR}/openapi.json',
    root_path=settings.ROOT_PATH
)

app.add_middleware(
    CORSMiddleware,
    allow_origins=settings.BACKEND_CORS_ORIGINS,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(upload.router, prefix=settings.API_V1_STR)
app.include_router(lines.router, prefix=settings.API_V1_STR)

In the lines.py, I have 2 GET endpoints:

  • /one-random-line --> returns a random line from a .txt file
  • /one-random-line-backwards --> should return the output of the /one-random-line

Since the output of the second GET endpoint should be the reversed string of the output of the first GET endpoint, I tried doing the following steps mentioned here

The codes:

import random

from fastapi import APIRouter, Request
from starlette.responses import RedirectResponse

router = APIRouter(
    prefix="/get-info",
    tags=["Get Information"],
    responses={
        200: {'description': 'Success'},
        400: {'description': 'Bad Request'},
        403: {'description': 'Forbidden'},
        500: {'description': 'Internal Server Error'}
    }
)


@router.get('/one-random-line')
def get_one_random_line(request: Request):
    lines = open('netflix_list.txt').read().splitlines()
    if request.headers.get('accept') in ['application/json', 'application/xml']:
        random_line = random.choice(lines)
    else:
        random_line = 'This is an example'
    return {'line': random_line}


@router.get('/one-random-line-backwards')
def get_one_random_line_backwards():
    url = router.url_path_for('get_one_random_line')
    response = RedirectResponse(url=url)
    return {'message': response[::-1]}

When I do this, I get the following error:

TypeError: 'RedirectResponse' object is not subscriptable

When I change the return of the second GET endpoint to return {'message': response}, I get the following output

enter image description here

What is the mistake I am doing?

Example:

If the output of /one-random-line endpoint is 'Maverick', then the output of /one-random-line-backwards should be 'kcirevam'

3
  • Refactor your code to move the common part out to a separate function, then call that function in both endpoints - it'll provide proper separation of concerns (the controller methods will be just controller methods handling the request, fetching the relevant data and returning it, while the function does the actual work). Don't think about it as calling another API, just refactor the common code into a function that can be used from both controller endpoints. Commented Aug 11, 2022 at 19:25
  • And a RedirectResponse is to tell an HTTP client that what they're looking for is somewhere else, not that you want to return the result from another endpoint - and you can't subscript a response, since it's not a list (or iterable). Commented Aug 11, 2022 at 19:26
  • Future readers looking for how to return a RedirectResponse instead, please see this answer and this answer. Also, if interested in making external API calls instead, please have a look at this answer. Commented Nov 21, 2024 at 6:46

3 Answers 3

6

You can just call any endpoint from your code directly as a function call, you don't have to deal with RedirectResponse() or anything. Below is an example of how this would look like and will run as is:

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/one-random-line")
async def get_one_random_line(request: Request):
    # implement your own logic here, this will only return a static line
    return {"line": "This is an example"}


@app.get("/one-random-line-backwards")
async def get_one_random_line_backwards(request: Request):
    # You don't have to do fancy http stuff, just call your endpoint:
    one_line = await get_one_random_line(request)
    return {"line": one_line["line"][::-1]}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Using curl we get the following result:

% curl localhost:8000/one-random-line          
{"line":"This is an example"}%     
% curl localhost:8000/one-random-line-backwards
{"line":"elpmaxe na si sihT"}%  
Sign up to request clarification or add additional context in comments.

Comments

1

Refactor your code to have the common part as a function you call - you'd usually have this in a module external to your controller.

# this function could live as LineService.get_random_line for example
# its responsibility is to fetch a random line from a file
def get_random_line(path="netflix_list.txt"):
    lines = open(path).read().splitlines()
    return random.choice(lines)


# this function encodes the rule that "if the accepted response is json or xml
# we do the random value, otherwise we return a default value"
def get_random_or_default_line_for_accept_value(accept, path="netflix_list.txt", default_value="This is an example"):
    if accept not in ("application/json", "application/xml"):
        return default_value

    return get_random_line(path=path)


@router.get('/one-random-line')
def get_one_random_line(request: Request):
    return {
        "line": get_random_or_default_line_for_accept_value(
            accept=request.headers.get('accept'),
        ),
    }


@router.get('/one-random-line-backwards')
def get_one_random_line_backwards(request: Request):
    return {
        "line": get_random_or_default_line_for_accept_value(
            accept=request.headers.get('accept'),
        )[::-1],
    }

4 Comments

Sorry, I see we both answered roughly at the same time. If you want, I will delete my answer (it is already available to OP through Github, where he asked the same question in the FastAPI issues).
More documentation is better; they both solve the same issue in different ways. I prefer to move the common functionality out to a separate function instead of using a controller endpoint as the source of truth, since that could have non-obvious consequences if you think you're just changing that single endpoint. However, in some cases it might be what you want. In this case the reverse endpoint will break if you change the key from line for example.
@MatsLindh Thanks for the answer. But in this case, the output of both the endpoints is different. Is it possible that the output of the one-random-line is passed on to one-random-line-backwards endpoint and is reversed? The line should be the same, but just reversed in the second endpoint.
Sorry, but I don't understand what the issue is. There is no state kept between requests in either case, so if you make a new request that retrieves a random line, it'll be selected at random in any case. If you don't match the supported content-types in Accept, a static text will be returned (and this will be reversed properly). If you want to first retrieve a string, then retrieve that same string returned, you need to either have some state for what string was returned last (and how you'd handle multiple clients), or you'll have to include the string to be reversed in your second request.
-1

Actually, it's possible to call an API endpoint from another endpoint of this API. You just need to call it from another thread. So, create a function for the call, and use something like this from inside the endpoint:

thread = threading.Thread(target=request_foo, args=(arg1, arg2))
thread.start()

Just know it's a bad practice. It's better to create shared code in the external file and use it from any endpoint you want.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.