How to create routes with FastAPI within a class
Asked Answered
M

9

35

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.

Morelos answered 11/9, 2020 at 20:20 Comment(0)
L
17

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)
Lucrece answered 13/9, 2020 at 11:12 Comment(3)
If you have session as a shared dependency, concurrent requests would share the same instance?Iron
The class instance is created and dependencies are called for each request independentlyLucrece
When I try to inject an instance of my own class like that, it throws an error saying that it was supposed to be a Pydantic-aware type o_O Is that expected?Flatways
B
56

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"}
Briony answered 3/1, 2022 at 9:37 Comment(1)
Hi again. I've posted a generic question on SO about how to solve dependency injection when the parameters originate from a CLI for a class-based fastapi. I'm still using your method but need this functionality and don't know how to make it work. The question is at #75450217Guacin
L
17

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)
Lucrece answered 13/9, 2020 at 11:12 Comment(3)
If you have session as a shared dependency, concurrent requests would share the same instance?Iron
The class instance is created and dependencies are called for each request independentlyLucrece
When I try to inject an instance of my own class like that, it throws an error saying that it was supposed to be a Pydantic-aware type o_O Is that expected?Flatways
S
9

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.

Soffit answered 28/1, 2021 at 2:26 Comment(2)
I like this approach a lot. Does this support async routes?Follower
Yes, it'll be work with all kinds of routes.Soffit
A
7

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.

Anhydrite answered 5/9, 2021 at 20:31 Comment(8)
I thank you very much for this package! @Olivier, I got an error when calling include_router, saying that their is no 'router attribute'. In the __init__(), shouldn't super().__init__() be called? If yes, this same example is also in the GitLab readme (so you don't forget).Pressmark
@mpc-DT thanks for noticing that. I'll fix it!Anhydrite
@OliverDain In my case, the args parameters come through if name == "main", which can call a main() function or configure under main. Either way, I get the error 'app' cannot be found. Pulling app=FastAPI() out of main() or main solves this but leaves 'app' as a global. Did I miss something?Guacin
@HenryThornton hard to say without seeing your code. It does appear that you've missed something, but I'm not sure what.Anhydrite
@OliverDain In your code above, app=FastApi() is in the main() function. How is main() called and how are the args passed to main()?Guacin
@HenryThornton up to you. You could do the normal if __name__ == '__main__' thing and then call it. In my example above, my main parses the argument by calling parse_args (first line of main) but you could parse elsewhere and pass the arguments in.Anhydrite
this is brilliant, the best class app for fastapiMitzvah
works well, but OpenApi throws an error for meEarshot
U
5

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)
Unifoliolate answered 25/1, 2021 at 3:53 Comment(1)
This "breaks" the navigation in your IDE, ie. you cannot hop into home() as it's declared and lost within the constructor's scope.Mondrian
S
1

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",
    )
)
Sprayberry answered 7/9, 2021 at 12:22 Comment(1)
This is a very nice answer. However, this actually fails with routers with parameters like "/{item_id}" and I don't know exactly why...Lovejoy
H
1

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)
Hospitalet answered 22/3, 2022 at 10:13 Comment(1)
What is the purpose the ProductionUseCase class?Hormuz
U
-1

The answer by @Gustavo Perena) can also be implemented as:

from fastapi 
import FastAPI, APIRouter

class Hello:

    def __init__(self, name: str):
        self.name = name
        self.router = APIRouter()
        self.router.get("/hello")(self.hello) # use decorator

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

app = FastAPI()
hello = Hello("World")
app.include_router(hello.router)
Unreel answered 6/4 at 6:4 Comment(0)
D
-3

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",
        )
Doughnut answered 11/11, 2020 at 7:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.