Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Better way to organize routes into multiple files #217

Closed
kenleejr opened this issue Aug 7, 2024 · 11 comments
Closed

Better way to organize routes into multiple files #217

kenleejr opened this issue Aug 7, 2024 · 11 comments

Comments

@kenleejr
Copy link

kenleejr commented Aug 7, 2024

Hi team, amazing project you've built! I currently have a large project where I don't want to put all my routes into one file because it gets too cumbersome. I prefer to have different python files for each page ala Streamlit. But then I'm left with this odd pattern of adding all the routes manually to the main app object in the python file which starts the uvicorn server. See below:

main.py
routes/
- Page1.py
- Page2.py

I will have routes defines in Page1.py and Page2.py but I can't decorate them with @app.route() in those files because that requires importing the app object into them, and having Page1.py, Page2.py, etc. run when the app starts. In order to have them run when the app starts I'd have to import the functions into main.py and that is a circular import. I have to do this in main.py:

from .routes.Page1 import data_sources_page, firestore_collection_selector, submit_data_sources
from .routes.Page2 import prompts_page, submit_prompt, qa_chatbot_page
app = FastHTML(hdrs=(tlink, dlink, zeromd_header), ws_hdr=True)
app.router.add_route("/data_sources_page", data_source_page, methods=["GET"])
app.router.add_route("/firestore_collection_selector", firestore_collection_selector, methods=["GET"])
app.router.add_route("/submit_data_sources", submit_data_sources, methods=["POST"])
app.router.add_route("/prompts_page", prompts_page, methods=["GET"])
app.router.add_route("/submit_prompt", submit_prompt, methods=["POST"])
app.router.add_route("/qa_chatbot_page", qa_chatbot_page, methods=["GET"])
...
if __name__ == '__main__': uvicorn.run("main:app", host='0.0.0.0', port=8000, reload=True)

For literally all routes in my app. It's unclear when looking at an individual Page1.py what functions are routes vs. normal functions and when looking at main.py it's unclear how these routes relate to the whole app.

@psabhay
Copy link
Contributor

psabhay commented Aug 7, 2024

In your routes/page.py you do something like -

from starlette.routing import Route

def get_page():
    # do whatever you like
    pass

routes = [Route("/page", endpoint=get_page, methods=["GET"])] # add all your routes in this list

Then in your main.py file you add the routes from other route files like this -

from fasthtml.common import *
from routes.page import routes as page_routes
from routes.blog import routes as blog_routes

app_routes = [*app_routes, *blog_routes] # add all your routes from other files

app, rt = fast_app(
    routes=app_routes,
)

I hope it helps :-)

@kenleejr
Copy link
Author

kenleejr commented Aug 8, 2024

Yes that is very helpful, thank you!

@kenleejr
Copy link
Author

kenleejr commented Aug 8, 2024

@psabhay I actually had to make an additional change to get this to work. The RouterX object within the FastHTML class calls the constructor of Starlette's base Router with the routes before it creates the RouteX routes from them:

When you call:

app, rt = fast_app(routes=app_routes)

This passes a list of normal Starlette Route objects to the constructor of RouterX which just passes them to Starlette Router through super()

class RouterX(Router):
    def __init__(self, routes=None, redirect_slashes=True, default=None, on_startup=None, on_shutdown=None,
                 lifespan=None, *, middleware=None, hdrs=None, ftrs=None, before=None, after=None, htmlkw=None, **bodykw):
        # This passes our routes to the Router prematurely, we actually want to convert them to RouteX routes first.
        super().__init__(routes, redirect_slashes, default, on_startup, on_shutdown,
                 lifespan=lifespan, middleware=middleware)
        self.hdrs,self.ftrs,self.bodykw,self.htmlkw,self.before,self.after = hdrs,ftrs,bodykw,htmlkw or {},before,after

So what we really need to do is create lists of RouteX (and WS_RouteX) objects at the end of each file and add them explicitly to the list of routes. Furthermore we need to pass in empty lists for before and after fields, because, normally these are listified from None in the constructor or the FastHTML object.

from fasthtml.fastapp import *

# route definitions
def get_page():
    # do whatever you like
    pass

routes = [RouteX("/page", endpoint=get_page, methods=["GET"], before=[], after=[])] # add all your routes in this list

In main.py:

from fasthtml.common import *
from routes.page import routes as page_routes
from routes.blog import routes as blog_routes

app_routes = [*app_routes, *blog_routes] # add all your routes from other files

app, rt = fast_app()

app.router.routes.extend(app_routes) # works

If you don't do these steps you get a bunch of errors:

@jph00
Copy link
Contributor

jph00 commented Aug 11, 2024

This is how I do it: https://github.com/AnswerDotAI/fh-about/blob/main/main.py

@jph00 jph00 closed this as completed Aug 11, 2024
@Pjt727
Copy link

Pjt727 commented Aug 17, 2024

This is how I do it:
make_app.py

from fasthtml.common import *

app, rt = fast_app()

page.py

from fasthtml.common import *
from make_app import app

@app.get("/foo", name="foo")
def foo():
    return Div("Hello world!", A("Self link", href=app.url_path_for("foo")))

main.py

from fasthtml.common import serve
from page import *

serve()

@ericfeunekes
Copy link

@jph00 just curious if you would use that approach if each of your sub-pages had more routes? E.g. if each page had a dozen or more routes, would you still import and define the routes in your main file as you do for the fh-about webpage, or would you take an approach more similar to @Pjt727 that doesn't require updating the main app when routes in a sub-page change.

@jph00
Copy link
Contributor

jph00 commented Aug 20, 2024

tbh originally i did it the same way as @Pjt727, but changed it because i figured it might be easier for people to follow and a bit more familiar. i think either is fine though.

@antoine-hachez
Copy link

FastAPI solved this issue in a cleaner way with APIRouter, which lets you create routes independently of the app and add them to the app later.

Unfortunately it does not seem very easy to implement because RouterX requires a reference to the app. I would be curious to know why.

@Karthik777
Copy link

This is what I've done and it works really well.
app.py

from fasthtml.common import *
app, rt = fast_app()

mp = MainPage(app)

main_page.py

class MainPage:

    def __init__(self, app: FastHTML):
        self.app = app
        self.rt = app.route
        self.setup_routes()

    def setup_routes(self):
        @self.rt("/login")
        @with_layout(welcome_layout)
        def login():
            return WelcomePage(Assets.svg_dir)()

        @self.rt("/auth/modal")
        def auth_modal(step: str = "login"):
            """Return the auth modal content."""
            return AuthForm(step=step)()

This way, I can implement all of routes as controllers and just link those controllers in the app.py. You could potentially have the first part of app.py as a make_app.py and import it to routes.py and add all controller classes and serve them.

Hopefully, this helps

@jph00
Copy link
Contributor

jph00 commented Dec 18, 2024

FYI FastHTML has an APIRouter nowadays too. :)

@Karthik777
Copy link

haha, I've never been more happy at chucking code I've written

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants