Skip to content

Commit e482c67

Browse files
authored
Merge pull request #15 from volfpeter/docs/path-parameters
Docs: path parameters / dynamic routes guide
2 parents cb633cc + 43afb79 commit e482c67

18 files changed

+513
-33
lines changed

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ Web development framework that brings the Next.js developer experience to Python
2626

2727
## Pre-requisite knowledge
2828

29-
To get started, all you need is a basic understanding of:
29+
*Don't be intimidated by this section. By the time you go through the [Application components](https://volfpeter.github.io/holm/application-components) document and the [Quick start guide](https://volfpeter.github.io/holm/guides/quick-start-guide), you will have a very good intuition of how to use `holm`.*
30+
31+
To get started, all you need is a *basic* understanding of:
3032

3133
- [FastAPI](https://fastapi.tiangolo.com/tutorial/): the underlying web framework.
3234
- [htmy](https://volfpeter.github.io/htmy/): the used component / templating and rendering library.
@@ -61,9 +63,9 @@ Similarly to Next.js, `holm` is built around the concept of file-system based ro
6163
- You do not need to manually define routes, every [application component](https://volfpeter.github.io/holm/application-components) is automatically discovered and registered in the application.
6264
- You do not need to manually wrap pages in their layouts, it is automatically done based on your application's code structure.
6365

64-
You can find all the necessary details on the [Application components](https://volfpeter.github.io/holm/application-components) page, and the [Quick start guide](https://volfpeter.github.io/holm/quick-start-guide) can walk you through the process of creating your first application. The two are complementary documents, reading both is strongly recommended.
66+
You can find all the necessary details on the [Application components](https://volfpeter.github.io/holm/application-components) page, and the [Quick start guide](https://volfpeter.github.io/holm/guides/quick-start-guide) can walk you through the process of creating your first application. The two are complementary documents, reading both is strongly recommended.
6567

66-
HTML rendering is also fully automated, in the vast majority of cases you do not need to concern yourself with that (the only exceptions are HTML APIs, more on that below).
68+
HTML rendering is also fully automated, in the vast majority of cases you do not need to concern yourself with that (the only exceptions are HTML APIs, more on that in the [Rendering APIs with HTMX](https://volfpeter.github.io/holm/guides/rendering-apis-with-htmx) guide).
6769

6870
All you need to do is follow these simple rules:
6971

@@ -72,13 +74,13 @@ All you need to do is follow these simple rules:
7274

7375
## Examples
7476

75-
If you prefer to learn through examples, the [Quick start guide](https://volfpeter.github.io/holm/quick-start-guide) is the best place to start.
77+
If you prefer to learn through examples, the [Quick start guide](https://volfpeter.github.io/holm/guides/quick-start-guide) is the best place to start.
7678

77-
To learn about creating rendering APIs and adding HTMX to your application, you should have a look at the [Rendering APIs with HTMX](https://volfpeter.github.io/holm/rendering-apis-with-htmx) guide.
79+
To learn about creating rendering APIs and adding HTMX to your application, you should have a look at the [Rendering APIs with HTMX](https://volfpeter.github.io/holm/guides/rendering-apis-with-htmx) guide.
7880

79-
The [Custom applications](https://volfpeter.github.io/holm/custom-applications) guide shows you how to customize the FastAPI application instance as well as HTML rendering.
81+
The [Custom applications](https://volfpeter.github.io/holm/guides/custom-applications) guide shows you how to customize the FastAPI application instance as well as HTML rendering.
8082

81-
For error handling examples, you should check out the [Error Handling](https://volfpeter.github.io/holm/error-handling) guide.
83+
For error handling examples, you should check out the [Error Handling](https://volfpeter.github.io/holm/guides/error-handling) guide.
8284

8385
You can discover even more features by exploring the [test application](https://github.com/volfpeter/holm/tree/main/test_app) of the project.
8486

docs/application-components.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,19 @@ Note on rendering APIs:
5050

5151
## Path parameters as package names
5252

53-
You can capture URL path parameters by using special package names. Two formats are supported:
53+
Routes with path parameters, also often referred to as "dynamic routes", are essential to almost all applications. A simple example is a user profile page, served at `/user/{id}` for example, where we want to display information about the user with the given ID.
54+
55+
You can capture dynamic URL path parameters by using special package names. Two formats are supported:
5456

5557
- `_param_` format: package names like `_id_` or `_user_id_`.
5658
- `{param}` format: package names like `{id}` or `{user_id}`. *(Note: it works with `holm`, but static code analysis tools flag it because `{id}` is not a valid Python identifier.)*
5759

5860
Both formats are converted to FastAPI path parameters. For example:
5961

60-
- File path `user/_id_/profile` becomes URL `/user/{id}/profile`.
61-
- File path `user/{user_id}/settings` becomes URL `/user/{user_id}/settings`.
62+
- File path `user/_id_/page.py` becomes URL `/user/{id}`.
63+
- File path `user/{user_id}/settings/page.py` becomes URL `/user/{user_id}/settings`.
6264

63-
Any layout or page within these packages can access the parameter as a FastAPI dependency by adding it to their function signature (e.g., `id: int` or `user_id: str`).
65+
Any layout or page within these packages can access the path parameter as a FastAPI dependency by adding it to their function signature (e.g., `id: int` or `user_id: str`).
6466

6567
## Private packages
6668

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ The below example demonstrates these steps as simply as possible, through creati
1919

2020
If you followed the [quick-start-guide](quick-start-guide.md), then you can make these changes in the `main.py` file and immediately see the result in action.
2121

22-
```python
22+
```python hl_lines="6-14 17 26"
2323
from fastapi import FastAPI
2424
from fasthx.htmy import HTMY
2525

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Before you continue, make sure you have the basic application from the [quick st
66

77
As a reminder, you must write standard FastAPI error handlers (async functions with a `Request` and an `Exception` argument), as you would in any FastAPI application. The only difference is that error handlers can return `htmy.Component`s as well as FastAPI `Response` objects.
88

9-
For additional rules and recommendations on error handling, please read the corresponding section of the [Application components](application-components.md) guide.
9+
For additional rules and recommendations on error handling, please read the corresponding section of the [Application components](../application-components.md) guide.
1010

1111
With all that said, let's set up error handling by adding the `error.py` file to the root of our project (next to `main.py`).
1212

docs/guides/path-parameters.md

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
# Path parameters and dynamic routing
2+
3+
This guide demonstrates how to create pages with dynamic routes in a `holm` application. We'll build a simple application that lists users and shows a profile page for each user, with the user ID as a path parameter (dynamic URL segment).
4+
5+
We will cover:
6+
7+
- How to create dynamic routes using `holm`'s file-system-based routing.
8+
- How to access path parameters in your page and metadata functions.
9+
- How to execute async code in pages, layouts, or metadata functions.
10+
- How to generate dynamic metadata for pages with dynamic routes.
11+
- How to use FastAPI dependencies to fetch data and share it between application components.
12+
13+
The entire source code of this application can be found in the [examples/path-parameters](https://github.com/volfpeter/holm/tree/main/examples/path-parameters) directory of the repository.
14+
15+
This guide focuses on path parameters and dynamic routing, and has a lot in common with the [Quick start guide](quick-start-guide.md). It is assumed you have the necessary dependencies installed, as described in the quick start guide.
16+
17+
## Application structure
18+
19+
Our application will have a home page, a page listing all users, and a dynamic page for individual user profiles. The corresponding file structure looks like this:
20+
21+
```text hl_lines="6-8"
22+
my_app/
23+
├── layout.py
24+
├── main.py
25+
├── page.py
26+
└── user/
27+
├── _id_/
28+
│ ├── __init__.py
29+
│ └── page.py
30+
├── __init__.py
31+
├── page.py
32+
└── service.py
33+
```
34+
35+
The most interesting part is the `user/_id_/page.py` module. `holm` interprets the `_id_` package name as a path parameter named `id`, and creates the `/user/{id}` route for it. It lets you have `id: IdType` dependencies in page, metadata, and layout functions in this package (including its subpackages).
36+
37+
## Application initialization
38+
39+
Let's start with `main.py`, our application's entry point. It's just two lines of code:
40+
41+
```python
42+
from holm import App
43+
44+
app = App()
45+
```
46+
47+
## Data model and services
48+
49+
Let's continue by implementing the `user/service.py` module. It acts as our data layer, and defines a `User` model and two `async` functions for fetching users. Think of it as a simple in-memory database with an `async` interface for demonstration.
50+
51+
```python hl_lines="4-5 13-16 19 29"
52+
from dataclasses import dataclass
53+
54+
55+
@dataclass(frozen=True, kw_only=True, slots=True)
56+
class User:
57+
"""User model."""
58+
59+
id: int
60+
name: str
61+
email: str
62+
63+
64+
users_by_id: dict[int, User] = {
65+
i: User(id=i, name=f"User {i}", email=f"user-{i}@holm.ccm") for i in range(10)
66+
}
67+
"""Dictionary that maps user IDs to the corresponding user objects."""
68+
69+
70+
async def list_users() -> list[User]:
71+
"""
72+
Lists all users.
73+
74+
The function is async to demonstrate how easily async tools, for example ORMs
75+
can be used in `holm`.
76+
"""
77+
return list(users_by_id.values())
78+
79+
80+
async def get_user(id: int) -> User | None:
81+
"""
82+
Returns the user with the given ID, if the user exists.
83+
84+
The function is async to demonstrate how easily async tools, for example ORMs
85+
can be used in `holm`.
86+
"""
87+
return users_by_id.get(id)
88+
```
89+
90+
We now have everything in place to start working on the user interface!
91+
92+
## Root layout and home page
93+
94+
When it comes to the user interface, we should start with the application's layout and home page (`layout.py` and `page.py`, next to `main.py`).
95+
96+
These are essentially the same as what we implemented in the [Quick start guide](quick-start-guide.md), with minor changes in the layout's `nav` element and page content, so we won't go into the details here.
97+
98+
Here is the layout (`layout.py`):
99+
100+
```python hl_lines="9-13 29-32"
101+
from htmy import Component, ComponentType, Context, component, html
102+
103+
from holm import Metadata
104+
105+
106+
@component
107+
def layout(children: ComponentType, context: Context) -> Component:
108+
"""Root layout wrapping all pages."""
109+
metadata = Metadata.from_context(context)
110+
title = "Admin App"
111+
# Let pages set only their subpage title, and add the application name automatically.
112+
if subpage_title := metadata.get("title"):
113+
title = f"{title} | {subpage_title}"
114+
115+
return (
116+
html.DOCTYPE.html,
117+
html.html(
118+
html.head(
119+
html.title(title),
120+
html.meta(charset="utf-8"),
121+
html.meta(name="viewport", content="width=device-width, initial-scale=1"),
122+
html.link( # Use PicoCSS to add some default styling.
123+
rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
124+
),
125+
),
126+
html.body(
127+
html.header(
128+
html.nav(
129+
html.ul(
130+
html.li(html.a("Home", href="/")),
131+
html.li(html.a("Users", href="/user")),
132+
)
133+
),
134+
class_="container",
135+
),
136+
html.main(children, class_="container"),
137+
html.footer(html.p("© 2025 Admin App"), class_="container"),
138+
class_="container-fluid",
139+
),
140+
),
141+
)
142+
```
143+
144+
And here is the home page (`page.py`, next to `main.py`):
145+
146+
```python
147+
from htmy import Component, html
148+
149+
# Static metadata for this page
150+
metadata = {"title": "Dashboard"}
151+
152+
153+
def page() -> Component:
154+
"""Home page content."""
155+
return html.div(
156+
html.h1("Welcome to Admin App"),
157+
html.p("This is a minimal holm application demonstrating:"),
158+
html.ul(
159+
html.li("How to use path parameters, also known as dynamic routing"),
160+
html.li("File-system based routing with dynamic routes"),
161+
),
162+
html.a("Navigate to the User page to start exploring", href="/user"),
163+
)
164+
```
165+
166+
## The user list page
167+
168+
Next, we'll create the users page at the `/user` URL (`user/page.py`).
169+
170+
This page uses the `list_users()` service to load the list of users, and displays them as a HTML list. Each list item contains an anchor tag that we can use to navigate to the user's profile page.
171+
172+
```python hl_lines="10 17"
173+
from htmy import Component, html
174+
175+
from .service import list_users
176+
177+
metadata: dict[str, str] = {"title": "Users"}
178+
179+
180+
async def page() -> Component:
181+
# Load users using an async service.
182+
users = await list_users()
183+
return html.div(
184+
html.h1("Users:"),
185+
html.ul(
186+
# Create list items with a link to the profile page for each user.
187+
# Don't forget to use the spread operator! html.ul expects its
188+
# children as positional arguments, not as a single list.
189+
*(html.li(html.a(user.name, href=f"/user/{user.id}")) for user in users),
190+
),
191+
)
192+
```
193+
194+
## User profile page
195+
196+
We have finally reached the essence of this guide: the user profile page. Don't expect any magic though.
197+
198+
As explained at the start, `holm` automatically creates the `/user/{id}` URL for the page in the `user/_id_/page.py` module, so you can have `id: int` dependencies in page and metadata (even layout) functions. Or, as you will see and as you may expect if you are familiar with FastAPI, dependencies that depend on `id: int` in some way. The `id` argument is of course resolved from the `id` path parameter.
199+
200+
With that said, let's create the user profile page:
201+
202+
```python hl_lines="8-12 15 26 36"
203+
from typing import Annotated
204+
205+
from fastapi import Depends
206+
from htmy import html
207+
208+
from ..service import User, get_user
209+
210+
# The `get_user()` function expects an `id: int`. The argument's name and type
211+
# matches the `{id}` path parameter, which means we can use `get_user()` as
212+
# a FastAPI dependency without any additional work (FastAPI will resolve the
213+
# `id: int` argument correctly from the `{id}` path parameter)! Let's do this.
214+
DependsUser = Annotated[User | None, Depends(get_user)]
215+
216+
217+
def metadata(id: int, user: DependsUser) -> dict[str, str]:
218+
"""
219+
Metadata function that uses the `DependsUser` annotated FastAPI dependency
220+
to get the user whose ID was submitted in the `{id}` path parameter (which
221+
is the URL segment corresponding to the `_id_` package name.)
222+
223+
Just to show that the user was indeed loaded based on the submitted ID,
224+
the metadata function also uses the `id: int` argument, that is resolved
225+
by FastAPI from the `{id}` path parameter.
226+
"""
227+
user_title = "Not Found" if user is None else user.name
228+
if user and user.id != id:
229+
raise ValueError(
230+
"id and user.id should match, because both values originate "
231+
"from the `{id}` path parameter the client submitted."
232+
)
233+
return {
234+
"title": f"User | {user_title}",
235+
}
236+
237+
238+
async def page(user: DependsUser) -> html.div:
239+
"""
240+
Page function that uses the `DependsUser` annotated FastAPI dependency
241+
to get the user whose ID was submitted in the `{id}` path parameter.
242+
243+
FastAPI resolves each dependency once, meaning we're only loading the user
244+
once, and we share it between application components through FastAPI's
245+
dependency injection system.
246+
"""
247+
if user is None:
248+
return html.div(
249+
html.h1("The user does not exist"),
250+
)
251+
252+
return html.div(
253+
html.strong("Name:"),
254+
html.span(user.name),
255+
html.strong("ID:"),
256+
html.span(str(user.id)),
257+
html.strong("Email:"),
258+
html.span(user.email),
259+
style="display: grid; grid-template-columns: max-content 1fr; gap: 1rem;",
260+
)
261+
```
262+
263+
Here is a summary of the key takeaways:
264+
265+
- Packages names of `_identifier_` format correspond to `{identifier}` URL segments.
266+
- `{identifier}` URL segments are FastAPI path parameters, and thus they enable dynamic routing.
267+
- The `get_user(id: int)` service function can be used as a FastAPI dependency to conveniently get access to the requested user in page, metadata, and layout functions. This is also efficient, because FastAPI resolves each dependency only once.
268+
269+
## Run the application
270+
271+
That's it! You can now run your application using `uvicorn`:
272+
273+
```bash
274+
uvicorn main:app --reload
275+
```
276+
277+
Or with `fastapi-cli` if installed:
278+
279+
```bash
280+
fastapi dev main.py
281+
```
282+
283+
You can now open your browser, navigate to `http://localhost:8000/`, and start exploring your application in action.

0 commit comments

Comments
 (0)