[REQ] [python-fastapi] Better support for spec-first development
Created by: sherifnada
Is your feature request related to a problem? Please describe.
The current python-fastapi generator seems to be geared towards a one-time generation of the python server rather than a continuous spec-first approach. For example:
Let's say I have the following OpenAPI spec
openapi: 3.0.0
info:
description: |
My API
version: "1.0.0"
title: my API
contact:
email: myemail@gmail.com
license:
name: MIT
url: "https://opensource.org/licenses/MIT"
paths:
/hello_world:
get:
description: "Sample endpoint"
responses:
"200":
description: Successful operation
content:
application/json:
schema:
type: object
properties:
hello:
type: string
When I run openapi-generator -i <path_to_spec> -g python-fastapi
, a file src/openapi/apis/default_api.py
(among other things) is autogenerated containing the following code:
router = APIRouter()
@router.get(
"/hello_world",
responses={
200: {"model": InlineResponse200, "description": "Successful operation"},
},
tags=["default"],
)
async def hello_world_get(
) -> InlineResponse200:
"""Sample endpoint"""
...
The problem with this code gen pattern is that it doesn't enable a spec-first workflow because it doesn't separate API contract declaration from its implementation. That is, if I implement the generated hello_world_get
method, then regenerate the file from the OpenAPI spec, my implementation gets overwritten.
If this was say, Java, the endpoints would be encapsulated under an interface like:
// DefaultAPI.java
// This interface is autogenerated
interface DefaultAPI() {
InlineResponse200 helloWorld();
}
Then in a separate file I'd implement the API
// DefaultApiImplementation.java
class DefaultApiImplementation implements DefaultAPI {
InlineResponse200 helloWorld() {
// ...
}
}
If a new method is added to the spec or an existing method is updated, I would run the generator and it would update DefaultAPI.java
with any new parameters, return types, API endpoints, etc... And because the implementation (which I create by hand) implements that interface, the build will fail if one of the signatures doesn't correctly match or if an endpoint is not implemented.
Describe the solution you'd like
I'd love to start a conversation about whether the generator intends to evolve in this direction or if I'm just misunderstanding its usage pattern. Ideally, the code generated above enables two development experience invariants:
- allow separating the implementation of api endpoints from their declaration
- detect if an API method in the spec was not implemented by the developer or implemented with incorrect signatures and raise an exception during CI or tests
I have some outlines of a solution and would love some feedback. Basically, we'd change the structure of the generator to have the following output components:
- for each API, there is an
ABC
declaring the methods a developer needs to implement. Each such method corresponds to one API endpoint. - For each API, the user implements a class which extends the
ABC
from step 1 in a separate file - The user manually has to write the
main.py
file which initializes theFastAPI
instance which actually launches the server, using the implementation they created from step 2 to initialize any routers.
I've included some pseudocode below that demoes what this would look like on the OpenAPI spec above:
default_api.py
(this file is autogenerated):
class APIInterface(ABC):
# autogenned, contains pythonic signature of api
@abstractmethod
async def hello_world():
""" hello world """
def initialize_router(api: APIInterface) -> APIRouter:
router = APIRouter()
# Directly add the method as a route implementation
# instead of adding via decoration (which is the current approach)
router.get(
"/hello_world",
responses={
200: {"model": InlineResponse200, "description": "Successful operation"},
},
tags=["default"],
)
# for each endpoint, do the same thing...
# ...
# at the end just return the router
return router
api_implementation.py
(this file is written and maintained by the user):
from default_api import APIInterface
# handwritten implementation of the API methods
class APIImplementation(APIInterface):
async def hello_world():
# code code code
main.py
(this file is written and maintained by the user, although we can probably automate some parts of it. Not essential that we do that for the sake of this discussion):
# The user must manually write this class
from default_api import initialize_router
from api_implementation import APIImplementation
app = FastAPI(
title="my API",
description=" API ",
version="1.0.0",
)
app.include_router(initialize_router(APIImplementation()))
Would love some feedback on this approach and whether there is any appetite for adopting it.