Skip to content

Refactor to use a more traditional CRUD module structure #104

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

Merged
merged 18 commits into from
Apr 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,14 @@ jobs:

- name: Set required env variables
run: |
echo "DB_USER=postgres" >> $GITHUB_ENV
echo "DB_PASSWORD=postgres" >> $GITHUB_ENV
echo "DB_HOST=127.0.0.1" >> $GITHUB_ENV
echo "DB_PORT=5432" >> $GITHUB_ENV
echo "DB_NAME=test_db" >> $GITHUB_ENV
echo "SECRET_KEY=$(openssl rand -base64 32)" >> $GITHUB_ENV
echo "BASE_URL=http://localhost:8000" >> $GITHUB_ENV
echo "RESEND_API_KEY=resend_api_key" >> $GITHUB_ENV
echo "DB_USER=postgres" > _environment
echo "DB_PASSWORD=postgres" >> _environment
echo "DB_HOST=127.0.0.1" >> _environment
echo "DB_PORT=5432" >> _environment
echo "DB_NAME=test_db" >> _environment
echo "SECRET_KEY=$(openssl rand -base64 32)" >> _environment
echo "BASE_URL=http://localhost:8000" >> _environment
echo "RESEND_API_KEY=resend_api_key" >> _environment

- name: Setup Graphviz
uses: ts-graphviz/setup-graphviz@v2
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@
__pycache__
*.pyc
.env
_environment
/.quarto/
_docs/
*.ipynb
.pytest_cache/
.mypy_cache/
node_modules
package-lock.json
package.json
.specstory
.cursorrules
.cursor
repomix-output.txt
artifacts/
1 change: 1 addition & 0 deletions .python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.13
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,15 @@ Make sure the development database is running and tables and default
permissions/roles are created first.

``` bash
uvicorn main:app --host 0.0.0.0 --port 8000 --reload
uv run uvicorn main:app --host 0.0.0.0 --port 8000 --reload
```

Navigate to http://localhost:8000/

### Lint types with mypy

``` bash
mypy .
uv run mypy .
```

## Developing with LLMs
Expand Down
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/.quarto/
105 changes: 4 additions & 101 deletions docs/architecture.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ with dot.subgraph(name='cluster_client') as client:
# Create server subgraph below
with dot.subgraph(name='cluster_server') as server:
server.attr(label='Server')
server.node('C', 'Convert to Pydantic model', fillcolor='lightgreen', style='rounded,filled')
server.node('D', 'Optional custom validation', fillcolor='lightgreen', style='rounded,filled')
server.node('C', 'FastAPI request validation in route signature', fillcolor='lightgreen', style='rounded,filled')
server.node('D', 'Business logic validation in route function body', fillcolor='lightgreen', style='rounded,filled')
server.node('E', 'Update database', fillcolor='lightgreen', style='rounded,filled')
server.node('F', 'Middleware error handler', fillcolor='lightgreen', style='rounded,filled')
server.node('G', 'Render error template', fillcolor='lightgreen', style='rounded,filled')
Expand Down Expand Up @@ -59,103 +59,6 @@ dot.render('static/data_flow', format='png', cleanup=True)

![Data flow diagram](static/data_flow.png)

The advantage of the PRG pattern is that it is very straightforward to implement and keeps most of the rendering logic on the server side. The disadvantage is that it requires an extra round trip to the database to fetch the updated data, and re-rendering the entire page template may be less efficient than a partial page update on the client side.
The advantage of the PRG pattern is that it is very straightforward to implement and keeps most of the rendering logic on the server side. One disadvantage is that it requires an extra round trip to the database to fetch the updated data, and re-rendering the entire page template may be less efficient than a partial page update on the client side. Another disadvantage is that it if the user makes an invalid form submission, they will see an error page and will have to click the browser's "back" button to get back to the form with their original form inputs.

## Form validation flow

We've experimented with several approaches to validating form inputs in the FastAPI endpoints.

### Objectives

Ideally, on an invalid input, we would redirect the user back to the form, preserving their inputs and displaying an error message about which input was invalid.

This would keep the error handling consistent with the PRG pattern described in the [Architecture](https://promptlytechnologies.com/fastapi-jinja2-postgres-webapp/docs/architecture) section of this documentation.

To keep the code DRY, we'd also like to handle such validation with Pydantic dependencies, Python exceptions, and exception-handling middleware as much as possible.

### Obstacles

One challenge is that if we redirect back to the page with the form, the page is re-rendered with empty form fields.

This can be overcome by passing the inputs from the request as context variables to the template.

But that's a bit clunky, because then we have to support form-specific context variables in every form page and corresponding GET endpoint.

Also, we have to:

1. access the request object (which is not by default available to our middleware), and
2. extract the form inputs (at least one of which is invalid in this error case), and
3. pass the form inputs to the template (which is a bit challenging to do in a DRY way since there are different sets of form inputs for different forms).

Solving these challenges is possible, but gets high-complexity pretty quickly.

### Approaches

The best solution, I think, is to use really robust client-side form validation to prevent invalid inputs from being sent to the server in the first place. That makes it less important what we do on the server side, although we still need to handle the server-side error case as a backup in the event that something slips past our validation on the client side.

Here are some patterns we've considered for server-side error handling:

<style>
.styled-table, .styled-table th, .styled-table td {
border: 1px solid black;
padding: 8px;
border-collapse: collapse;
}

.styled-table th:nth-child(1) { width: 50%; }
.styled-table th:nth-child(2),
.styled-table th:nth-child(3),
.styled-table th:nth-child(4) { width: 15%; }
.styled-table th:nth-child(5) { width: 10%; }
</style>

<table class="styled-table">
<thead>
<tr>
<th>Approach</th>
<th>Returns to same page</th>
<th>Preserves form inputs</th>
<th>Follows PRG pattern</th>
<th>Complexity</th>
</tr>
</thead>
<tbody>
<tr>
<td>Validate with Pydantic dependency, catch and redirect from middleware (with exception message as context) to an error page with "go back" button</td>
<td>No</td>
<td>Yes</td>
<td>Yes</td>
<td>Low</td>
</tr>
<tr>
<td>Validate in FastAPI endpoint function body, redirect to origin page with error message query param</td>
<td>Yes</td>
<td>No</td>
<td>Yes</td>
<td>Medium</td>
</tr>
<tr>
<td>Validate in FastAPI endpoint function body, redirect to origin page with error message query param and form inputs as context so we can re-render page with original form inputs</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>High</td>
</tr>
<tr>
<td>Validate with Pydantic dependency, use session context to get form inputs from request, redirect to origin page from middleware with exception message and form inputs as context so we can re-render page with original form inputs</td>
<td>Yes</td>
<td>Yes</td>
<td>Yes</td>
<td>High</td>
</tr>
<tr>
<td>Validate in either Pydantic dependency or function endpoint body and directly return error message or error toast HTML partial in JSON, then mount error toast with HTMX or some simple layout-level Javascript</td>
<td>Yes</td>
<td>Yes</td>
<td>No</td>
<td>Low</td>
</tr>
</tbody>
</table>

Presently this template primarily uses option 1 but also supports option 2. Ultimately, I think option 5 will be preferable; support for that [is planned](https://github.com/Promptly-Technologies-LLC/fastapi-jinja2-postgres-webapp/issues/5) for a future update or fork of this template.
A future iteration of this application will use HTMX to update the page in place, so that on an invalid submission an error toast is displayed without a page reload (thus preserving the user's scroll position and form inputs).
12 changes: 8 additions & 4 deletions docs/contributing.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ To contribute code to the project:

### Rendering the documentation

The README and documentation website are rendered with [Quarto](https://quarto.org/docs/). If you ,make changes to the `.qmd` files in the root folder and the `docs` folder, run the following commands to re-render the docs:
The README and documentation website are rendered with [Quarto](https://quarto.org/docs/). If you make changes to the `.qmd` files in the root folder and the `docs` folder, you will need to re-render the docs with Quarto.

Quarto expects environment variables to be set in a file called `_environment`, so before running Quarto render commands, you should copy your `.env` file to `_environment`.

``` bash
# To copy the .env file to _environment
cp .env _environment
# To render the documentation website
quarto render
uv run quarto render
# To render the README
quarto render index.qmd --output-dir . --output README.md --to gfm
uv run quarto render index.qmd --output-dir . --output README.md --to gfm
```

Due to a quirk of Quarto, an unnecessary `index.html` file is created in the root folder when the README is rendered. This file can be safely deleted.
Expand All @@ -74,5 +78,5 @@ When creating new features,
To publish the documentation to GitHub Pages, run the following command:

``` bash
quarto publish gh-pages
uv run quarto publish gh-pages
```
Loading