How to create routes with FastAPI within a class

Question:

So I need to have some routes inside a class, but the route methods need to have the self attr (to access the class’ attributes).
However, FastAPI then assumes self is its own required argument and puts it in as a query param

This is what I’ve got:

app = FastAPI()
class Foo:
    def __init__(y: int):
        self.x = y

    @app.get("/somewhere")
    def bar(self): return self.x

However, this returns 422 unless you go to /somewhere?self=something. The issue with this, is that self is then str, and thus useless.

I need some way that I can still access self without having it as a required argument.

Asked By: Nexy7574

||

Answers:

For creating class-based views you can use @cbv decorator from fastapi-utils. The motivation of using it:

Stop repeating the same dependencies over and over in the signature of related endpoints.

Your sample could be rewritten like this:

from fastapi import Depends, FastAPI
from fastapi_utils.cbv import cbv
from fastapi_utils.inferring_router import InferringRouter


def get_x():
    return 10


app = FastAPI()
router = InferringRouter()  # Step 1: Create a router


@cbv(router)  # Step 2: Create and decorate a class to hold the endpoints
class Foo:
    # Step 3: Add dependencies as class attributes
    x: int = Depends(get_x)

    @router.get("/somewhere")
    def bar(self) -> int:
        # Step 4: Use `self.<dependency_name>` to access shared dependencies
        return self.x


app.include_router(router)
Answered By: alex_noname

You inherit from FastAPI in your class and use the FastAPI decorators as method calls (I am going to show it using APIRouter, but your example should work anlog):

class Foo(FastAPI):
    def __init__(y: int):
        self.x = y

        self.include_router(
            health.router,
            prefix="/api/v1/health",
        )
Answered By: sysid

I put routes to def __init__. It works normally.
Example:

from fastapi import FastAPI
from fastapi.responses import HTMLResponse

class CustomAPI(FastAPI):
    def __init__(self, title: str = "CustomAPI") -> None:
        super().__init__(title=title)

        @self.get('/')
        async def home():
            """
            Home page
            """
            return HTMLResponse("<h1>CustomAPI</h1><br/><a href='/docs'>Try api now!</a>", status_code=status.HTTP_200_OK)
Answered By: Khiem Tran

I didn’t like the standard way of doing this, so I wrote my own library. You can install it like this:

$ pip install cbfa

Here is an example of how to use it:

from typing import Optional
from fastapi import FastAPI
from pydantic import BaseModel
from cbfa import ClassBased


app = FastAPI()
wrapper = ClassBased(app)

class Item(BaseModel):
    name: str
    price: float
    is_offer: Optional[bool] = None

@wrapper('/item')
class Item:
    def get(item_id: int, q: Optional[str] = None):
        return {"item_id": item_id, "q": q}

    def post(item_id: int, item: Item):
        return {"item_name": item.name, "item_id": item_id}

Note that you don’t need to wrap decorators around each method. It is enough to name the methods according to their purpose in the HTTP protocol. The whole class is turned into a decorator.

Answered By: Evgeniy Blinov

I’ve just released a project that lets you use a class instance for route handling with simple decorators. cbv is cool but the routing is on the class itself, not instances of the class. Being able to use a class instance lets you do dependency injection in a way that feels simpler and more intuitive to me.

For example, the following works as expected:

from classy_fastapi import Routable, get, delete

class UserRoutes(Routable):
   """Inherits from Routable."""

   # Note injection here by simply passing values
   # to the constructor. Other injection frameworks also 
   # supported as there's nothing special about this __init__ method.
   def __init__(self, dao: Dao) -> None:
      """Constructor. The Dao is injected here."""
      super().__init__()
      self.__dao = Dao

   @get('/user/{name}')
   def get_user_by_name(name: str) -> User:
      # Use our injected DAO instance.
      return self.__dao.get_user_by_name(name)

   @delete('/user/{name}')
   def delete_user(name: str) -> None:
      self.__dao.delete(name)


def main():
    args = parse_args()
    # Configure the DAO per command line arguments
    dao = Dao(args.url, args.user, args.password)
    # Simple intuitive injection
    user_routes = UserRoutes(dao)
    
    app = FastAPI()
    # router member inherited from Routable and configured per the annotations.
    app.include_router(user_routes.router)

You can find it on PyPi and install via pip install classy-fastapi.

Answered By: Oliver Dain

Another approach is to have a decorator class that takes parameters. The routes are registered before and added at run-time:

from functools import wraps

_api_routes_registry = []


class api_route(object):
    def __init__(self, path, **kwargs):
        self._path = path
        self._kwargs = kwargs

    def __call__(self, fn):
        cls, method = fn.__repr__().split(" ")[1].split(".")
        _api_routes_registry.append(
            {
                "fn": fn,
                "path": self._path,
                "kwargs": self._kwargs,
                "cls": cls,
                "method": method,
            }
        )

        @wraps(fn)
        def decorated(*args, **kwargs):
            return fn(*args, **kwargs)

        return decorated

    @classmethod
    def add_api_routes(cls, router):
        for reg in _api_routes_registry:
            if router.__class__.__name__ == reg["cls"]:
                router.add_api_route(
                    path=reg["path"],
                    endpoint=getattr(router, reg["method"]),
                    **reg["kwargs"],
                )

And define a custom router that inherits the APIRouter and add the routes at __init__:

class ItemRouter(APIRouter):
    @api_route("/", description="this reads an item")
    def read_item(a: str = "de"):
        return [7262, 324323, a]

    @api_route("/", methods=["POST"], description="add an item")
    def post_item(a: str = "de"):
        return a

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        add_api_routes(self)


app.include_router(
    ItemRouter(
        prefix="/items",
    )
)
Answered By: Joost Döbken

This can be done by using an APIRouter‘s add_api_route method:

from fastapi import FastAPI, APIRouter


class Hello:

    def __init__(self, name: str):
        self.name = name
        self.router = APIRouter()
        self.router.add_api_route("/hello", self.hello, methods=["GET"])

    def hello(self):
        return {"Hello": self.name}


app = FastAPI()
hello = Hello("World")
app.include_router(hello.router)

Example:

$ curl 127.0.0.1:5000/hello
{"Hello":"World"}

add_api_route‘s second argument (endpoint) has type Callable[..., Any], so any callable should work (as long as FastAPI can find out how to parse its arguments HTTP request data). This callable is also known in the FastAPI docs as the path operation function (referred to as "POF" below).

Why decorating methods doesn’t work

WARNING: Ignore the rest of this answer if you’re not interested in a technical explanation of why the code in the OP’s answer doesn’t work

Decorating a method with @app.get and friends in the class body doesn’t work because you’d be effectively passing Hello.hello, not hello.hello (a.k.a. self.hello) to add_api_route. Bound and unbound methods (a.k.a simply as "functions" since Python 3) have different signatures:

import inspect
inspect.signature(Hello.hello)  # <Signature (self)>
inspect.signature(hello.hello)  # <Signature ()>

FastAPI does a lot of magic to try to automatically parse the data in the HTTP request (body or query parameters) into the objects actually used by the POF.

By using an unbound method (=regular function) (Hello.hello) as the POF, FastAPI would either have to:

  1. Make assumptions about the nature of the class that contains the route and generate self (a.k.a call Hello.__init__) on the fly. This would likely add a lot of complexity to FastAPI and is a use case that FastAPI devs (understandably) don’t seem interested in supporting. It seems the recommended way of dealing with application/resource state is deferring the whole problem to an external dependency with Depends.

  2. Somehow be able to generate a self object from the HTTP request data (usually JSON) sent by the caller. This is not technically feasible for anything other than strings or other builtins and therefore not really usable.

What happens in the OP’s code is #2. FastAPI tries to parse the first argument of Hello.hello (=self, of type Hello) from the HTTP request query parameters, obviously fails and raises a RequestValidationError which is shown to the caller as an HTTP 422 response.

Parsing self from query parameters

Just to prove #2 above, here’s a (useless) example of when FastAPI can actually "parse" self from the HTTP request:

(Disclaimer: Do not use the code below for any real application)

from fastapi import FastAPI

app = FastAPI()

class Hello(str):
    @app.get("/hello")
    def hello(self):
        return {"Hello": self}

Example:

$ curl '127.0.0.1:5000/hello?self=World'
{"Hello":"World"}
Answered By: Gustavo Bezerra

In this case I’m able to wire controller using python class and use a collaborator passing it by dep injection.

Here full example plus tests

class UseCase:
    @abstractmethod
    def run(self):
        pass


class ProductionUseCase(UseCase):
    def run(self):
        return "Production Code"


class AppController:

    def __init__(self, app: FastAPI, use_case: UseCase):
        @app.get("/items/{item_id}")
        def read_item(item_id: int, q: Optional[str] = None):
            return {
                "item_id": item_id, "q": q, "use_case": use_case.run()
            }


def startup(use_case: UseCase = ProductionUseCase()):
    app = FastAPI()
    AppController(app, use_case)
    return app


if __name__ == "__main__":
    uvicorn.run(startup(), host="0.0.0.0", port=8080)
Answered By: Marco Sabatini

Try this:

https://github.com/fangorntreabeard/fastapi_classrouter/blob/main/main.py

Solves the problem of not quite convenient documentation of fastapi code (takes documentation from docstring, + creates a response model based on class parameters), partially avoids duplication of HTTP resources, can determine the availability of http methods out of the box based on their names, can work with placeholders.

Answered By: farngorntreebeard