Home > Net >  Keeping endpoint function declarations in separate modules with FastAPI
Keeping endpoint function declarations in separate modules with FastAPI

Time:11-26

I have an API that uses FastAPI. In a single file (main.py), I have the call to the function that creates the API

from fastapi import FastAPI
# ...
app = FastAPI()

As well as all the endpoints:

@app.post("/sum")
async def sum_two_numbers(number1: int, number2: int):
    return {'result': number1   number2}

But as the application gets larger, the file is becoming messy and hard to maintain. The obvious solution would be to keep function definitions in separate modules and just import them and use them in main.py, like this:

from mymodules.operations import sum_two_numbers
# ...
@app.post("/sum")
sum_two_numbers(number1: int, number2: int)

Only that doesn't work. I don't know if I'm doing it wrong or it can't be done, but I get this error from VSCode:

Expected function or class declaration after decorator | Pylance

(My program has so many errors that I haven't seen the actual interpreter complaint, but if that's important, I can try debug it and post it here)

So is this impossible to do and I have to stick to the one-file API, or it is possible to do, but I'm doing it wrong? If the second, what is the right way?

CodePudding user response:

So is this impossible to do and I have to stick to the one-file API, or it is possible to do, but I'm doing it wrong?

One file api is just for demo/test purposes, in the real world you always do multi-file api especialy with framework like fastapi where you use different type of file like pydantic models, db models, etc.

If the second, what is the right way?

There is no "right way", there is ways that fit your needs. You can follow the advenced user guide, to see a good example.

what the doc suggest:

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│       ├── __init__.py
│       └── admin.py

what i use when i have db, pydantic models, etc

.
├── app
│   ├── __init__.py
│   ├── main.py
│   ├── dependencies.py
│   └── routers
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── models
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── schemas
│   │   ├── __init__.py
│   │   ├── items.py
│   │   └── users.py
│   └── internal
│   │   ├── __init__.py
│   │   └── admin.py

here models represent db models and schemas represent pydantic models

CodePudding user response:

The common solution is to split your application into subrouters. Instead of using app directly when registering your views, you create an instance APIRouter (from fastapi import APIRouter) inside each of your modules, then you register these subrouters into your main application.

Inside a dedicated api module, such as api/pages.py:

from fastapi import APIRouter

router = APIRouter()

@router.get('')
async def get_pages():
  return ...
from .api import (
    pages,
    posts,
    users,
)


app.include_router(pages.router, prefix='/pages')
app.include_router(posts.router, prefix='/posts')
app.include_router(users.router, prefix='/users')

Another powerful construct you can use is to have two dedicated base routers, one that requires authentication and one that doesn't:

unauthenticated_router = APIRouter()
authenticated_router = APIRouter(dependencies=[Depends(get_authenticated_user)])

.. and you can then register the different routes under each router, depending on whether you want to guard the route with an authenticated user or not. You'd have two subrouters inside each module, one for endpoints that require authentication and one for those that doesn't, and name them appropriately (and if you have no public endpoints, just use authenticated_router as the single name).

unauthenticated_router.include_router(authentication.router, prefix='/authenticate')
unauthenticated_router.include_router(users.unauthenticated_router, prefix='/users', tags=['users'])
authenticated_router.include_router(users.router, prefix='/users')

Any sub router registered under authenticated_router will have the get_authenticated_user dependency evaluated first, which in this case would throw a 401 error if the user wasn't logged in. You can then authorized further based on roles etc. in the dependencies for the view function - but this makes it very explicit whether you want your endpoint to end up in a chain that requires authentication or not.

  • Related