Skip to content
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
13 changes: 6 additions & 7 deletions .env
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
# ENV
PYTHON_VERSION="313" # which dockerfile to use. see in dockerfiles/python*/Dockerfile
PYTHON_VERSION=314 # which dockerfile to use. see in dockerfiles/python*/Dockerfile

# App config.

API_USERNAME="ubuntu"
API_PASSWORD="debian"
API_USERNAME=ubuntu
API_PASSWORD=debian

# To get a string like this run:
# openssl rand -hex 32
API_SECRET_KEY="09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7"
API_ALGORITHM="HS256"
# infinity
API_ACCESS_TOKEN_EXPIRE_MINUTES="5256000000"
API_SECRET_KEY=09d25e094faa6ca2556c818166b7a9563b93f7099f6f0f4caa6cf63b88e8d3e7
API_ALGORITHM=HS256
API_ACCESS_TOKEN_EXPIRE_MINUTES=30
4 changes: 2 additions & 2 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ updates:
- package-ecosystem: "pip" # See documentation for possible values.
directory: "/" # Location of package manifests.
schedule:
interval: "monthly"
interval: "weekly"

# Maintain dependencies for GitHub Actions.
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
interval: "weekly"
7 changes: 3 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,19 @@ jobs:

strategy:
matrix:
python-version: [311, 312, 313]
python-version: [313, 314]

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Spin up container
run: |
sed -i "s/PYTHON_VERSION=.*/PYTHON_VERSION=${{ matrix.python-version }}/" '.env'
echo "Building container for Python ${{ matrix.python-version }}."
cat
docker compose up -d

- name: Wait and check the health of the container
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,16 @@ jobs:
# Use matrix strategy to run the tests on multiple Py versions on multiple OSs.
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.11", "3.12", "3.13"]
python-version: ["3.13", "3.14"]

steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}

- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v8.1.0
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.13
3.14
79 changes: 49 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![logo](https://user-images.githubusercontent.com/30027932/134270064-baecfbec-b3e7-4cb7-a07e-c11a58526260.png)

[![Mentioned in Awesome <INSERT LIST NAME>](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/mjhea0/awesome-fastapi#boilerplate)
[![License](https://img.shields.io/cocoapods/l/AFNetworking?style=flat-square)](https://github.com/rednafi/think-asyncio/blob/master/LICENSE)
[![License](https://img.shields.io/github/license/rednafi/fastapi-nano?style=flat-square)](https://github.com/rednafi/fastapi-nano/blob/master/LICENSE)

</div>

Expand All @@ -16,22 +16,28 @@ directory structure.

- Uses [FastAPI][fastapi] to build the HTTP API endpoints.

- Served via [Gunicorn](gunicorn) with multiple [Uvicorn][uvicorn] workers. Uvicorn is a
lightning-fast "ASGI" server. It runs asynchronous Python web code in a single process.
- Served with the [FastAPI CLI][fastapi_cli] using the `fastapi run` command, which
runs [Uvicorn][uvicorn] under the hood.

- Simple reverse-proxying with [Caddy][caddy].

- OAuth2 (with hashed password and Bearer with JWT) based authentication.
- OAuth2 authentication with [Argon2][argon2] password hashing via [pwdlib][pwdlib]
and Bearer JWT tokens via [PyJWT][pyjwt].

- [CORS (Cross Origin Resource Sharing)][cors] enabled.

- Flask inspired divisional directory structure, suitable for small to medium backend
development.

- Uses [uv][uv] for dependency management, enabling shorter build time.
- Uses [FastAPI standard dependencies][fastapi_standard] and [uv][uv] for dependency
management.

- Dockerized using **python:3.13-slim** image and optimized for size. Dockerfile for
Python 3.12 and 3.11 can also be found in the `dockerfiles` directory.
- Uses [Pydantic Settings][pydantic_settings] for typed environment configuration.

- Supports Python 3.14 and 3.13.

- Dockerized with multi-stage Dockerfiles based on **python:3.14-slim** by default.
Dockerfiles for Python 3.14 and 3.13 can be found in the `dockerfiles` directory.

## Quickstart

Expand Down Expand Up @@ -61,7 +67,7 @@ If you want to run the app locally, without using Docker, then:
```

This will set up a virtual environment `.venv` in the current directory with Python
3.13, install dependencies, and start the Uvicorn server.
3.14, install dependencies, and start the FastAPI development server.

### Explore the endpoints

Expand All @@ -76,8 +82,8 @@ If you want to run the app locally, without using Docker, then:
![Screenshot from 2020-06-21 22-15-18][screenshot_1]

- Press the `authorize` button on the right and add _username_ and _password_. The APIs
use OAuth2 (with hashed password and Bearer with JWT) based authentication. In this
case, the username and password is `ubuntu` and `debian` respectively.
use OAuth2 with Argon2 password hashing and Bearer JWT authentication. In this case,
the username and password are `ubuntu` and `debian` respectively.

![Screenshot from 2020-06-21 22-18-25][screenshot_2]

Expand Down Expand Up @@ -106,7 +112,7 @@ If you want to run the app locally, without using Docker, then:
-d "username=ubuntu&password=debian" | jq -r ".access_token")"
```

This should show a response like this:
This should show a response like this. The random values will vary.

```json
{
Expand All @@ -118,7 +124,7 @@ If you want to run the app locally, without using Docker, then:

### Housekeeping

- Run tests with `make tests` (uses [pytest][pytest]).
- Run tests with `make test` (uses [pytest][pytest]).
- Lint with [ruff] and check types with [mypy] using `make lint`.
- Update dependencies with `make dep-update`.
- Stop containers with `make kill-container`.
Expand All @@ -141,18 +147,19 @@ fastapi-nano
│ │ └── submod.py # submodule of api_b package
│ ├── core # this is where the configs live
│ │ ├── auth.py # authentication with OAuth2
│ │ ├── config.py # sample config file
│ │ ├── config.py # typed environment settings
│ │ └── __init__.py # empty init file to make the config folder a package
│ ├── __init__.py # empty init file to make the app folder a package
│ ├── main.py # main file where the fastAPI() class is called
│ ├── __init__.py # empty init file to make the svc folder a package
│ ├── main.py # main file where the FastAPI() class is called
│ ├── routes # this is where all the routes live
│ │ └── views.py # file containing the endpoints for api_a and api_b
│ └── tests # test package
│ ├── __init__.py # empty init file to make the tests folder a package
│ ├── test_api.py # integration testing the API responses
│ ├── test_apis.py # integration testing the API responses
│ ├── test_logger.py # unit testing logger configuration
│ └── test_functions.py # unit testing the underlying functions
├── dockerfiles # directory containing all the dockerfiles
├── .env # env file containing app variables
├── dockerfiles # Dockerfiles for supported Python versions
├── .env # env file containing app variables and Docker Python target
├── Caddyfile # simple reverse-proxy with caddy
├── docker-compose.yml # docker-compose file
├── pyproject.toml # pep-518 compliant config file
Expand All @@ -167,12 +174,12 @@ APIs in the template is to demonstrate how you can decouple the logics of multip
then assemble their endpoints in the routes directory. The following snippets show the logic
behind the dummy APIs.

This is a dummy submodule that houses a function called `random_gen` which generates a
This is a dummy submodule that houses a function called `rand_gen` which generates a
dictionary of random integers.

```python
# This a dummy module
# This gets called in the module_main.py file
# This is a dummy module.
# This gets called in mainmod.py.
from __future__ import annotations
import random

Expand All @@ -191,7 +198,7 @@ The `main_func` in the primary module calls the `rand_gen` function from the sub

```python
from __future__ import annotations
from svc.api_a.submod import rand_gen
from svc.apis.api_a.submod import rand_gen


def main_func(num: int) -> dict[str, int]:
Expand All @@ -204,20 +211,28 @@ The endpoint is exposed like this:
```python
# svc/routes/views.py
from __future__ import annotations
#... codes regarding authentication ...

from typing import Annotated

from fastapi import Depends

from svc.core.auth import UserInDB, get_current_user

CurrentUser = Annotated[UserInDB, Depends(get_current_user)]

# endpoint for api_a (api_b looks identical)
@router.get("/api_a/{num}", tags=["api_a"])
async def view_a(num: int, auth: Depends =Depends(get_current_user)) -> dict[str, int]:
async def view_a(num: int, _auth: CurrentUser) -> dict[str, int]:
return main_func_a(num)
```

So hitting the API with a random integer will give you a response like the following:
So hitting the API with a random integer will give you a response like the following. The
random values will vary.

```json
{
"seed": 22,
"random_first": 27,
"random_first": 5,
"random_second": 20
}
```
Expand All @@ -226,7 +241,7 @@ So hitting the API with a random integer will give you a response like the follo

- You can put your own API logic following the shape of `api_a` and `api_b` packages.
You'll have to add additional directories like `api_a` or `api_b` if you need to expose
more endponts.
more endpoints.

- Then expose the API URLs in the `routes/views.py` file. You may choose to create
multiple `views` files to organize your endpoint URLs.
Expand All @@ -249,9 +264,13 @@ So hitting the API with a random integer will give you a response like the follo
[cors]: https://fastapi.tiangolo.com/tutorial/cors/
[docker]: https://www.docker.com/
[fastapi]: https://fastapi.tiangolo.com/
[fastapi_security]: https://fastapi.tiangolo.com/tutorial/security/
[gunicorn]: https://gunicorn.org/
[httpx]: https://www.python-httpx.org/
[argon2]: https://argon2-cffi.readthedocs.io/
[fastapi_cli]: https://fastapi.tiangolo.com/fastapi-cli/
[fastapi_security]: https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/
[fastapi_standard]: https://fastapi.tiangolo.com/#standard-dependencies
[pydantic_settings]: https://fastapi.tiangolo.com/advanced/settings/
[pwdlib]: https://github.com/frankie567/pwdlib
[pyjwt]: https://pyjwt.readthedocs.io/
[pytest]: https://docs.pytest.org/en/stable/
[ruff]: https://astral.sh/ruff
[uvicorn]: https://uvicorn.org/
Expand Down
6 changes: 4 additions & 2 deletions bin/Dockerfile-template
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ SHELL ["sh", "-exc"]
# Set working directory
WORKDIR /app

ENV UV_LINK_MODE=copy

# Install dependencies
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \
Expand Down Expand Up @@ -40,5 +42,5 @@ ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 5001

# Entry point for running the application
ENTRYPOINT ["gunicorn", "svc.main:app", "--workers", "2", "--worker-class", \
"uvicorn.workers.UvicornWorker", "-b", "0.0.0.0:5001" ]
ENTRYPOINT ["fastapi", "run", "--workers", "2", "svc/main.py", \
"--host", "0.0.0.0", "--port", "5001" ]
8 changes: 3 additions & 5 deletions bin/generate_dockerfile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,19 @@ set -euo pipefail
# Template Dockerfile path
template_dockerfile="bin/Dockerfile-template"

# Read the content of the template Dockerfile (python 3.13)
# Read the content of the Dockerfile template.
if [[ ! -f "$template_dockerfile" ]]; then
echo "Template Dockerfile does not exist: $template_dockerfile"
exit 1
fi

# Read the content of the Dockerfile template
dockerfile_content=$(<"$template_dockerfile")

# Python versions to generate Dockerfiles for
python_versions=("3.11" "3.12" "3.13")
python_versions=("3.13" "3.14")

# Corresponding directories for each version
directories=("dockerfiles/python311" "dockerfiles/python312" "dockerfiles/python313")
directories=("dockerfiles/python313" "dockerfiles/python314")

# Loop over the Python versions and directories
for i in "${!python_versions[@]}"; do
Expand All @@ -28,7 +27,6 @@ for i in "${!python_versions[@]}"; do
# Create the directory if it doesn't exist
mkdir -p "$dir"

# Replace only the ARG PYTHON_VERSION=3.13 line with the specific version
updated_content="${dockerfile_content//ARG PYTHON_VERSION=bleh/ARG PYTHON_VERSION=$version}"

# Save the new Dockerfile
Expand Down
2 changes: 1 addition & 1 deletion bin/update_deps.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ rm -f uv.lock
rm -rf .venv || true

# Create a new virtual environment.
uv venv -p 3.13
uv venv -p 3.14

# Install the latest versions of the dependencies.
uv lock && uv sync
10 changes: 0 additions & 10 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ services:
dockerfile: ./dockerfiles/python${PYTHON_VERSION}/Dockerfile
labels:
- fnano
volumes:
- type: bind
source: .
target: /code
entrypoint:
- /bin/sh
- -c
- |
gunicorn svc.main:app --workers 2 \
--worker-class uvicorn.workers.UvicornWorker -b 0.0.0.0:5001
ports:
- "5001:5001"

Expand Down
44 changes: 0 additions & 44 deletions dockerfiles/python312/Dockerfile

This file was deleted.

Loading
Loading