[](https://github.com/AlbanAndrieu/fastapi-sample) fastapi-sample

September 7, 2026 · View on GitHub

FastAPI reference service for REST APIs, MCP/A2A, search integrations and homelab observability. The canonical runtime is deployed on FastAPI Cloud; Vercel provides a lightweight HTTP compatibility proxy.

Table of contents

// spell-checker:disable

// spell-checker:enable

Initialize

Requirements

Required tooling:

  • Python 3.13;
  • uv for dependency and virtual-environment management;
  • Node.js 24 and npm 10 for repository tooling;
  • Docker only for optional local infrastructure.

Install pre-commit through the locked Python environment; do not install a second global copy.

Install fastapi-sample as a developer

Using uv

Install dependencies from the lockfile into .venv, then run CLI tools through uv run so they use that environment (avoids ModuleNotFoundError for packages like pybreaker when a global fastapi binary points at another Python).

uv sync --frozen
uv run pre-commit install
uv run fastapi dev --port 8080

Use the complete ASGI entrypoint when testing MCP and lifespan resources:

uv run uvicorn server_all:app --reload --host 0.0.0.0 --port 8080
uv run pytest --no-cov

Keep UNLEASH_ENABLED=false for local development unless UNLEASH_INSTANCE_ID, UNLEASH_API_URL and UNLEASH_APP_NAME are configured from the GitLab feature-flag settings. Missing or placeholder credentials no longer start the Unleash polling thread.

Cypher Uncypher env variable

# Cypher
# DOTENV
cp .env.secrets secrets.env.sops
sops -e -i secrets.env.sops
# Uncypher
# YAML
sops -d secrets-enc.yaml
# DOTENV
sops -d secrets.env.sops > .env.sops.secrets

If no mise
source .env.secrets

Getting started

sequenceDiagram
    actor User as User Client
    participant HAProxy as HAProxy
    participant Traefik as Traefik
    participant KrakenD as KrakenD
    participant API as sample API Service

    autonumber
    User ->> HAProxy: HTTP Request ( https://krakend.nabla.com/sample/threads)
    HAProxy ->> Traefik: Forward Request (Add jm-client-ip)
    Traefik ->> Traefik: Resolve (krakend.nabla.com -> kraken.service.gra.uat.consul -> IP and PORT)
    Traefik ->>+ KrakenD: Forward Request (resolve kraken.service.gra.uat.consul)
    alt is jwt
    KrakenD ->> KrakenD: Check its Config (Get JWT public key URL)
    KrakenD ->> Traefik: New Request : Get JWT public key (resolve keycloak.service.gra.uat.consul)
    Traefik ->> Traefik: Resolve (keycloak.service.gra.uat.consul -> IP and PORT)
    Traefik ->>+ Keycloak: Get JWT public key
    Keycloak -->>- KrakenD: Forward Response (JWT public key)
    KrakenD ->> KrakenD: Valid Token (using JWT public key)
    end
    KrakenD ->> KrakenD: Check its config  (sample/threads -> sample.service.gra.uat.consul/threads)
    KrakenD ->>- Traefik: New Request (https://sample.service.gra.uat.consul/threads)
    Traefik ->> Traefik: Resolve (sample.service.gra.uat.consul -> IP and PORT)
    Traefik ->>+ API: Forward Request (https://sample.service.gra.uat.consul/threads)
    API -->>- KrakenD: Response (A json)
    KrakenD -->> Traefik: Forward Response
    Traefik -->> HAProxy: Forward Response
    HAProxy -->> User: HTTP Response

Fix redis cluster : All slots are not covered after query all startup_nodes

sudo service redis-server start

redis-cli -c -h localhost -p 6379
localhost:6379> PING
PONG

# cluster-enabled yes
redis-cli --cluster fix 127.0.0.1:6379

# export REDIS_HOST=localhost
make up-uvicorn

curl --request GET http://127.0.0.1:8080/ping
curl --request GET http://127.0.0.1:8080/metrics

curl --request GET http://127.0.0.1:8080/v1/external-api

Logfire observability

Pydantic Logfire tracing is enabled only when LOGFIRE_TOKEN is present. Create the write token in the nabla/fastapi-sample project and store it as a secret in FastAPI Cloud; never commit it. The optional LOGFIRE_ENVIRONMENT value makes production and local telemetry easier to filter.

export LOGFIRE_TOKEN="<write-token>"
export LOGFIRE_ENVIRONMENT="production"

For local OAuth authentication against the EU region:

logfire --base-url='https://logfire-eu.pydantic.dev' auth
logfire --base-url='https://logfire-eu.pydantic.dev' projects use \
  --org 'nabla' 'fastapi-sample'

Request and response headers, parsed request values, health checks, and metrics are not sent to Logfire. Without a token, the application starts normally and uses its existing OpenTelemetry configuration.

MCP clients (e.g. OpenRAG) and A2A

See Application entry points and local dashboards for the current ASGI, OpenAPI, MCP and Compose URLs.

  • Outbound MCP: set MCP_CLIENTS to a JSON array of stdio servers, for example:

    [
      {
        "name": "openrag",
        "command": "uvx",
        "args": ["openrag-mcp"],
        "env": {
          "OPENRAG_API_KEY": "your-key",
          "OPENRAG_URL": "http://localhost:3000"
        }
      }
    ]
    

    With a server named openrag, the deep agent gains LangChain tools openrag_search and openrag_chat that call MCP tools openrag_search / openrag_chat.

  • Ops HTTP (optional lock): set MCP_OPS_KEY and send header X-MCP-Ops-Key for GET /v1/mcp/ops/servers, GET /v1/mcp/ops/servers/{name}/tools, POST /v1/mcp/ops/servers/{name}/call.

  • A2A: set A2A_ENABLED=true and install deps from the api-ai group. The app mounts JSON-RPC at /a2a and the agent card at /a2a/.well-known/agent-card.json. Set A2A_PUBLIC_BASE_URL so the card lists a public JSON-RPC URL (e.g. https://api.example.com).

docs metrics openapi mcp

export OTEL_SDK_DISABLED=true

export DD_SERVICE="fastapi-sample"
export DD_ENV="nabla"
export DD_LOGS_INJECTION=true
export DD_TRACE_ENABLED=true
export DD_TRACE_SAMPLE_RATE="1"
export DD_PROFILING_ENABLED=true
export DD_APPSEC_ENABLED=true
export DD_IAST_ENABLED=true
export DD_APPSEC_SCA_ENABLED=true
export DD_GIT_COMMIT_SHA="$(git rev-parse HEAD)"
# git config --get remote.origin.url
export DD_GIT_REPOSITORY_URL="$(git config --get remote.origin.url)"

make up-gunicorn

DEBUG=1 uv run uvicorn server_all:app --reload --workers 1 --host 0.0.0.0 --port 8080

DD_TRACE_ENABLED and DD_PROFILING_ENABLED are independent and default to false. The profiler is started and stopped with the FastAPI lifespan. The application does not set a global Datadog user or send PII; user context should only be attached later from an authenticated request identity.

uv sync
uv run fastapi dev --port 8080

health

sudo lsof -ni:8080 -sTCP:ESTABLISHED
netstat -tlnp | grep 8080
sudo lsof -i :8080

Vite UI

cd vue-client/
npm run dev

Test JWT

Get the public key from keycloak [keycloak-uat]((http://account.int.albandrieu.com/realms/nabla)

or keycloak-dev keycloak-admin

and put it to key.pem

Get the bearer token valid-jwt

Go on back

Get from cookie, access_token

Validate JWT validate-jwt

# Go on back https://back.albandrieu.com/welcome
# Get from cookie access_token
# export JWT_TOKEN=$(curl -k "http://fastapi-sample.fastapicloud.dev/en/api/valid-jwt")
# export JWT_TOKEN=$(curl -k "https://nabla.front.albandrieu.com/en/api/valid-jwt")

# http://keycloak-admin.albandrieu.com/realms/nabla/

export JWT_TOKEN="eyJhbGcXXX"

curl -k -H "Authorization: Bearer $JWT_TOKEN" -X GET https://fastapi-sample.albandrieu.com/

# token is expired
#  {"Hello":"World"}

curl -k -i -X POST -H "Origin: https://nabla.front.albandrieu.com" \
    -H 'Content-Type: text/plain' \
    -H "Authorization: Bearer $JWT_TOKEN" \
    --data "{}" \
    "https://authorization.albandrieu.com/v1/token/upgrade"

Test

curl -k -fsSL https://fastapi-sample.albandrieu.com/
curl -k -v -I -H "X-Demo: test" -X GET  https://fastapi-sample.albandrieu.com/
curl -k -H "X-Demo: test" -X GET https://fastapi-sample.albandrieu.com/ | jq
curl -k -verbose -I -H "X-Forwarded-For: 1.1.1.1" -H 'Content-Type: application/json' -X GET  http://fastapi-sample.albandrieu.com/

[io_task][http://0.0.0.0:8080/io_task)

Result available on pyroscope

Jupiter

gitlab-data/data-science

User guide

Installation and commands

Python

python3 ./nabla/tools/get_data.py

python3 ./my-app/src/get_redis.py

Database demo

Create PostgreSQL postgres on pg-gra.albandrieu.com with Alembic

# Create/Upgrade schema
alembic upgrade head
alembic downgrade -1

Create PostgreSQL fastapi_sample_gitlab on pg-gra.albandrieu.com by hand

psql -h pg-gra.albandrieu.com -U postgres
CREATE USER fastapisample WITH PASSWORD 'XXX';
ALTER ROLE fastapisample WITH LOGIN;
CREATE USER back WITH PASSWORD 'XXX';
ALTER ROLE back WITH LOGIN;
-- create database fastapi_sample_gitlab with owner fastapisample encoding 'UTF8';
create database fastapi_sample_dev with owner fastapisample encoding 'UTF8';
# ALTER USER fastapisample PASSWORD 'XXX';
GRANT ALL ON SCHEMA public TO fastapisample;
GRANT ALL ON TABLE public.note TO fastapisample;
GRANT ALL ON TABLE public.sensor_reading TO fastapisample;
GRANT ALL ON TABLE public."user" TO fastapisample;
GRANT SELECT, USAGE, UPDATE ON SEQUENCE public.sensor_reading_id_seq TO fastapisample;
-- GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE public.note TO fastapisample;

# for alembic
DB_USER="postgres"
DB_PASS="password-reset-XXX" # nosec
# otherwise classic connection
DB_URL="postgresql://postgres:password-reset-XXX@127.0.0.1:5432/fastapi_sample_dev" # nosec
# Remove asyncpg for alembic to be able to init DB as fastapisample
DB_URL="postgresql://fastapisample:password-reset-XXX@127.0.0.1:5432/fastapi_sample_dev" # nosec

Vercel compatibility proxy

The Python application is deployed on FastAPI Cloud. Vercel is configured as a lightweight external rewrite to that canonical runtime; it must not bundle the full Python dependency graph, which exceeds Vercel's function-size limit.

The Vercel project follows this GitHub repository. Only main is built: the ignoreCommand in vercel.json skips branch and pull-request previews, which would otherwise try to package an application that exceeds the function-size limit. Pull requests remain validated by GitHub Actions. Use the FastAPI Cloud URL directly for WebSockets and MCP streaming instead of relying on the HTTP compatibility proxy.

The /api landing page publishes Open Graph and Twitter Card metadata. Its 1200×630 image is served at /api/assets/open-graph.png by FastAPI Cloud and through the Vercel rewrite.

Temporal demo

Temporal

uv sync --group temporal
uv run python nabla/temporalio/activities.py
uv run python worker.py
uv run python starter.py

Defect Dojo Parameters

dd_product

dd_product_types

All parameters need to be provided as environment variables:

ParameterRe-import findingsImport languagesRemark
DD_URLMandatoryMandatoryBase URL of the DefectDojo instance
DD_API_KEYMandatoryMandatoryShall be defined as a secret, eg. a protected variable in GitLab or an encrypted secret in GitHub
DD_PRODUCT_TYPE_NAMEMandatoryMandatoryIf a product type with this name does not exist, it will be created
DD_PRODUCT_NAMEMandatoryMandatoryIf a product with this name does not exist, it will be created
DD_ENGAGEMENT_NAMEMandatory-If an engagement with this name does not exist for the given product, it will be created
DD_ENGAGEMENT_TARGET_STARTOptional-Format: YYYY-MM-DD, default: today. The target start date for a newly created engagement.
DD_ENGAGEMENT_TARGET_ENDOptional-Format: YYYY-MM-DD, default: 2999-12-31. The target start date for a newly created engagement.
DD_TEST_NAMEMandatory-If a test with this name does not exist for the given engagement, it will be created
DD_TEST_TYPE_NAMEMandatory-From DefectDojo's list of test types, eg. Trivy Scan
DD_FILE_NAMEOptionalMandatory
DD_ACTIVEOptional-Default: true
DD_VERIFIEDOptional-Default: true
DD_MINIMUM_SEVERITYOptional-
DD_GROUP_BYOptional-Group by file path, component name, component name + version
DD_PUSH_TO_JIRAOptional-Default: false
DD_CLOSE_OLD_FINDINGSOptional-Default: true
DD_CLOSE_OLD_FINDINGS_PRODUCT_SCOPEOptional-Default: false
DD_DO_NOT_REACTIVATEOptional-Default: false
DD_VERSIONOptional-
DD_ENDPOINT_IDOptional-
DD_SERVICEOptional-
DD_BUILD_IDOptional-
DD_COMMIT_HASHOptional-
DD_BRANCH_TAGOptional-
DD_API_SCAN_CONFIGURATION_IDOptional-Id of the API scan configuration for API based parsers, e.g. SonarQube
DD_SOURCE_CODE_MANAGEMENT_URIOptional-
DD_SSL_VERIFYOptionalOptionalDisable SSL verification by setting to false or 0. Default: true
DD_EXTRA_HEADER_1OptionalOptionalIf extra header key is needed for auth in wafs or similar
DD_EXTRA_HEADER_1_VALUEOptionalOptionalThe corresponding value for extra header key
DD_EXTRA_HEADER_2OptionalOptionalIf extra header key is needed for auth in wafs or similar
DD_EXTRA_HEADER_2_VALUEOptionalOptionalThe corresponding value for extra header key

Quality check

python -m flake8  nabla --max-line-length=88 --max-complexity=30

ruff check --output-format gitlab > report_ruff.json && ruff format --check

pyright --outputjson > report_raw.json
pyright-to-gitlab-ci --src report_raw.json --output report_pyright.json --base_path .

trigger error in sentry-debug sentry

Sentry observability

The TrueNAS runtime can send errors to the self-hosted Sentry edge at 172.17.0.24:9005, while FastAPI Cloud can continue using an independently configured Sentry Cloud DSN.

Self-hosted and SaaS Sentry deployments have different project IDs and public keys. Therefore SENTRY_LOCAL_DSN must be configured explicitly; the application never derives self-hosted credentials from SENTRY_DSN.

Create a dedicated fastapi-sample project in the self-hosted sentry organization, then configure its public DSN in the TrueNAS runtime:

sudo docker exec -it ix-sentry-sentry-web-1 \
  sentry createproject \
    --name fastapi-sample \
    --platform python \
    --organization sentry

The command prints the new project's public DSN. Store that value outside Git and use the TrueNAS host edge as the DSN host:

SENTRY_LOCAL_DSN=http://<public-key>@172.17.0.24:9005/<project-id>
SENTRY_DSN=https://<cloud-public-key>@<cloud-ingest-host>/<cloud-project-id>
SENTRY_ENVIRONMENT=homelab
SENTRY_TRACES_SAMPLE_RATE=0.1
SENTRY_PROFILES_SAMPLE_RATE=0.0
SENTRY_ERROR_SAMPLE_RATE=1.0
SENTRY_MAX_BREADCRUMBS=50
SENTRY_SHUTDOWN_TIMEOUT=2

Selection is intentionally local-first only when SENTRY_LOCAL_DSN is explicitly configured and its host/port are reachable. Otherwise the application falls back to SENTRY_DSN. Reachability is only a transport probe; valid project credentials are guaranteed by requiring a real self-hosted DSN rather than rewriting a SaaS DSN.

When LOGFIRE_TOKEN is non-empty, Sentry continues to receive errors but its logs, traces, and profiles are disabled to avoid duplicate telemetry. Events and logs redact common secrets, and performance transactions for /health, /healthz, /sickz, and /metrics are discarded.

Sentry's native Python SDK is the single exporter for logs and traces. Do not also point the application's legacy OTLP exporter at Sentry, because that would duplicate telemetry.

Utility scripts

python3 nabla/loki/influxdb.py

# Create/Upgrade schema
alembic upgrade head

# Add header in file
# user_id,email text,last_login,cgu_read_and_accepted,roles
python3 scripts.py ~/Downloads/product-activity-2023-10-02.csv

Installation and commands

GOnpm run dev

go version
go mod init example.com/m/v2
go mod tidy
go run hello-world.go
go build hello-world.go
ls
./hello-world

Update README.md

npm install -g markdown-toc
markdown-toc README.md -i
markdown-toc CHANGELOG.md -i