This is a clean and minimal Python REST API template built with FastAPI and Uvicorn. It is designed to be used as a starting point for small API services, standalone HTTP backends that bots and other clients call over the network instead of bundling heavy dependencies locally.
- FastAPI with automatic OpenAPI documentation at
/docs. - Bearer token authentication shared across all protected endpoints. A missing or wrong token gets
401 Unauthorized, and tokens are compared in constant time. - A
GET /healthendpoint for uptime monitoring, which the Docker image also uses as its health check. - Configuration via environment variables using
pydantic-settings. No values are hardcoded, and the service refuses to start with a placeholder or short secret. - Structured JSON logging with a configurable log level. Every line is one JSON object, including uvicorn's access log.
- One version, set in
pyproject.toml, which the health endpoint and the OpenAPI docs read from the installed package. pyproject.tomlwith thehatchlingbuild backend, managed with uv and locked inuv.lock.Dockerfileanddocker-compose.ymlfor containerized deployment.- A test suite with
pytestcovering health, auth rejection, a template endpoint, and the setup and run scripts. - Tests, linting, formatting, strict type checking, and a Docker build run in CI on every push to
mainand on every pull request through the shared dev-standards workflow.
- Docker and Docker Compose for containerized setup.
Running without Docker requires Python 3.14 and uv. On Windows, install uv with winget install --id astral-sh.uv.
The setup script prepares the project in a single run, and it is safe to run again at any time.
On Windows, double-click setup.bat or run it from a terminal:
setup.bat
On macOS or Linux, run the following commands:
chmod +x setup.sh
./setup.sh
The script asks before it installs anything, and it does the following:
- It installs uv when uv is missing. uv also provides Python 3.14 when the machine lacks it.
- It offers to install Docker, and in a Git clone also the tools that the Docker image includes for running outside Docker. It uses winget on Windows, Homebrew on macOS, and the system package manager on Linux. On Windows it also turns on WSL, which Docker Desktop needs, and says when Windows needs a restart or virtualization is turned off in the firmware. It shows the computer's RAM and offers to cap the memory of Docker Desktop's virtual machine, at a suggested or your own size.
- In a Git clone, it runs
uv sync, which installs the package and its locked dependencies into.venv. - It copies
.env.templateto.envon the first run and fillsAPI_SECRETwith a random value.
A step that fails says what went wrong, why it matters, and what to do next, and the summary at the end lists it again.
The steps live in scripts/bootstrap.py, which needs only the Python standard library.
If you prefer to perform the setup manually, follow these steps:
uv sync
cp .env.template .env
# Edit .env and set API_SECRET and other values as needed.
uv run uvicorn api_template.main:app --reloadAfter setup has run once, the run script starts the API.
Double-click run.bat on Windows, or run ./run.sh on macOS and Linux.
It starts the API in Docker in the background, waits until its health check passes, and shows its status.
A downloaded release runs the image that GitHub publishes for every release, and a Git clone builds the image from its own files.
The container then starts again whenever Docker starts.
The script also takes an action, such as run.bat stop on Windows or ./run.sh stop elsewhere:
| Action | What it does |
|---|---|
start |
Starts everything in Docker and waits until it is ready. It is the default. |
stop |
Stops the containers. They stay stopped until the next start. |
status |
Shows whether each container runs and is healthy, and its version. In a downloaded release, it also shows whether updates are automatic. |
logs |
Follows the logs. Press Ctrl+C to stop following. |
update |
Installs the newest release. In a Git clone, it pulls the latest code, rebuilds on fresh base images, and restarts, then deletes the build cache that no build used for a week. |
schedule |
Installs new releases automatically every night at 04:00. It works only in a downloaded release. |
unschedule |
Stops installing new releases automatically. |
local |
Runs the project in the terminal without Docker. Press Ctrl+C to stop it. |
help |
Lists every action. |
When a service crashes right after it starts, the script shows the end of its log and stops it, so it does not restart over and over.
In a downloaded release, the update action downloads the newest image and takes the run, setup, and compose files, the scripts, README.md, and .env.template from it.
.env and the other settings stay as they are.
When the new release fails to start, it puts the previous image and files back, and later updates skip that release.
The schedule action runs this update every night at 04:00, and each run writes what it did into update.log. With UPDATE_WEBHOOK_URL in .env set to a Discord webhook, an update that installs a new release or fails also posts a short message to that webhook's channel, naming the new versions or the problem. A night without a new release posts nothing. To get a webhook, open a channel's settings in Discord, choose Integrations, then Webhooks, then New Webhook, and copy its URL.
Alternatively, you can run the API as a Docker container.
-
Copy
.env.templateto.envand setAPI_SECRET. -
Build and start the container:
docker compose up --build
The API listens on port 8000 by default.
The service keeps no state, so the container needs no volume.
Docker keeps at most two log files of 5 MB and overwrites the oldest. The container may use at most 1 GB of memory. That is several times its normal use, so only a leak reaches it, and the container then restarts on its own.
All configuration is read from environment variables or from a .env file in the project root.
| Variable | Required | Default | Description |
|---|---|---|---|
API_SECRET |
Yes | None | Shared bearer token of at least 16 characters. Callers must send this value in the Authorization header. Generate one with python -c "import secrets; print(secrets.token_urlsafe(32))". |
LOG_LEVEL |
No | INFO |
Log verbosity. Accepts DEBUG, INFO, WARNING, ERROR, or CRITICAL. Every log line, including uvicorn's access log, is one JSON object. |
UPDATE_WEBHOOK_URL |
No | Not set | A Discord webhook that the nightly update posts to when it installs a new release or fails. The run script reads it, not the service. It works like a password. |
api-template/
├── src/api_template/
│ ├── main.py # FastAPI application, lifespan, and route definitions.
│ ├── config.py # This service's settings on top of ServiceSettings.
│ ├── service.py # Shared settings, secret validation, and the version lookup.
│ ├── logging_config.py # JSON logging for every logger, including uvicorn's.
│ ├── request_id.py # Request IDs from X-Request-ID, which every log line of a request names.
│ ├── auth.py # Bearer token dependency used to protect endpoints.
│ └── models.py # Pydantic request and response models.
├── tests/
│ ├── test_api.py # Tests for this service's own routes.
│ ├── test_bootstrap.py # Tests for the setup scripts' steps. Core file.
│ ├── test_run.py # Tests for the run scripts' actions. Core file.
│ └── test_shared.py # Tests for the shared auth, settings, logging, and version code.
├── Dockerfile
├── docker-compose.yml
├── pyproject.toml # Project metadata and dependencies.
├── uv.lock # Locked dependency versions.
├── ruff.toml # Lint and format settings on top of the shared baseline.
├── .template-manifest.toml # Core files that derived APIs keep identical to this template.
├── setup.bat # Windows setup script.
├── setup.sh # macOS and Linux setup script.
├── scripts/bootstrap.py # The steps that both setup scripts run.
├── scripts/run.py # The actions that both run scripts take.
├── run.bat # Windows run script.
├── run.sh # macOS and Linux run script.
└── .env.template # Template for environment variables.
Use the GitHub template button to create a new repository based on this project. Then follow these steps to customize it:
-
Rename the package. In
pyproject.toml, change the projectnameand thepackagespath under[tool.hatch.build.targets.wheel]. Rename thesrc/api_template/directory to match, such assrc/my_service/. The source files import each other relatively, so onlytests/test_api.pyand theCMDline inDockerfilename the package. Also set the new name aspackageunder[tool.dev-standards.template]inpyproject.tomland asknown-first-partyinruff.toml. SetSERVICEinmain.pyto the new project name, because the version is read from the installed project of that name. Then runuv syncsouv.lockrecords the new name. -
Add your dependencies. Edit the
dependencieslist inpyproject.toml. -
Add configuration variables. Extend the
Settingsclass inconfig.pyand document new variables in.env.template. -
Add startup and shutdown logic. Use the
lifespancontext manager inmain.pyto load models, open connections, or perform any one-time initialization. -
Add endpoints. Define new routes in
main.py. Usedependencies=[Depends(require_auth)]to protect them. Add request and response models tomodels.py. -
Remove the template endpoint. Delete the
/template/echoroute and theTemplateRequest/TemplateResponsemodels once you have your own routes in place. -
Update
docker-compose.yml. Setname:to the new project name andimage:to the image that the new repository publishes, such asghcr.io/<owner>/<repository>:latest, so its containers and nightly update stay apart from other APIs. Change the host side of8000:8000to the port assigned to your service, such as8001:8000. The container itself keeps listening on port 8000. -
Write tests. Add test functions to
tests/test_api.py. Leavetests/test_shared.pyunchanged, because it is a core file.
uv run pytest
uv run mypy srcRun every lint and format check with uvx pre-commit run --all-files, or install the hooks once with uvx pre-commit install so they run on each commit.
The coding, prose, and commit conventions are documented in dev-standards.
All endpoints except /health and FastAPI's interactive docs at /docs, /redoc, and /openapi.json require a bearer token in the Authorization header.
A request without the header or with a wrong token gets 401 Unauthorized with a WWW-Authenticate: Bearer header:
POST /template/echo HTTP/1.1
Authorization: Bearer your-secret-here
Content-Type: application/json
{"text": "hello"}A Python client calling this API can use httpx.AsyncClient with the token set as a default header:
import httpx
client = httpx.AsyncClient(
base_url="http://localhost:8000",
headers={"Authorization": "Bearer your-secret-here"},
)This project is licensed under the MIT License. You may use, change, and share it, as long as every copy keeps the copyright notice and the license text.