{"id":20845392,"url":"https://github.com/rafsaf/minimal-fastapi-postgres-template","last_synced_at":"2026-02-10T00:04:58.675Z","repository":{"id":40404372,"uuid":"408249394","full_name":"rafsaf/minimal-fastapi-postgres-template","owner":"rafsaf","description":"minimal-fastapi-postgres-template based on official template but rewritten","archived":false,"fork":false,"pushed_at":"2026-02-03T18:36:33.000Z","size":1460,"stargazers_count":556,"open_issues_count":7,"forks_count":79,"subscribers_count":5,"default_branch":"main","last_synced_at":"2026-02-04T06:54:51.480Z","etag":null,"topics":["asyncio","boilerplate","fastapi","fastapi-boilerplate","fastapi-template","postgres","postgresql","python","sqlalchemy","template","template-repository"],"latest_commit_sha":null,"homepage":"https://minimal-fastapi-postgres-template.rafsaf.pl","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/rafsaf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2021-09-19T22:19:22.000Z","updated_at":"2026-02-01T00:04:18.000Z","dependencies_parsed_at":"2024-01-29T23:51:33.071Z","dependency_job_id":"924de901-e9a0-428b-ac56-407cbe67de88","html_url":"https://github.com/rafsaf/minimal-fastapi-postgres-template","commit_stats":null,"previous_names":[],"tags_count":13,"template":true,"template_full_name":null,"purl":"pkg:github/rafsaf/minimal-fastapi-postgres-template","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafsaf%2Fminimal-fastapi-postgres-template","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafsaf%2Fminimal-fastapi-postgres-template/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafsaf%2Fminimal-fastapi-postgres-template/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafsaf%2Fminimal-fastapi-postgres-template/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/rafsaf","download_url":"https://codeload.github.com/rafsaf/minimal-fastapi-postgres-template/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/rafsaf%2Fminimal-fastapi-postgres-template/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29286895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-09T21:57:15.303Z","status":"ssl_error","status_checked_at":"2026-02-09T21:57:11.537Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asyncio","boilerplate","fastapi","fastapi-boilerplate","fastapi-template","postgres","postgresql","python","sqlalchemy","template","template-repository"],"created_at":"2024-11-18T02:12:48.675Z","updated_at":"2026-02-10T00:04:58.649Z","avatar_url":"https://github.com/rafsaf.png","language":"Python","readme":"[![Live example](https://img.shields.io/badge/live%20example-https%3A%2F%2Fminimal--fastapi--postgres--template.rafsaf.pl-blueviolet)](https://minimal-fastapi-postgres-template.rafsaf.pl/)\n[![License](https://img.shields.io/github/license/rafsaf/minimal-fastapi-postgres-template)](https://github.com/rafsaf/minimal-fastapi-postgres-template/blob/main/LICENSE)\n[![Python 3.13](https://img.shields.io/badge/python-3.13-blue)](https://docs.python.org/3/whatsnew/3.13.html)\n[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)\n[![Tests](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml/badge.svg)](https://github.com/rafsaf/minimal-fastapi-postgres-template/actions/workflows/tests.yml)\n\n_Check out online example: https://minimal-fastapi-postgres-template.rafsaf.pl, it's 100% code used in template (docker image) with added my domain and https only._\n\n# Minimal async FastAPI + PostgreSQL template\n\n- [Minimal async FastAPI + PostgreSQL template](#minimal-async-fastapi--postgresql-template)\n  - [Features](#features)\n  - [Quickstart](#quickstart)\n    - [1. Create repository from a template](#1-create-repository-from-a-template)\n    - [2. Install dependecies with Poetry](#2-install-dependecies-with-poetry)\n    - [3. Setup database and migrations](#3-setup-database-and-migrations)\n    - [4. Now you can run app](#4-now-you-can-run-app)\n    - [5. Activate pre-commit](#5-activate-pre-commit)\n    - [6. Running tests](#6-running-tests)\n  - [About](#about)\n  - [Step by step example - POST and GET endpoints](#step-by-step-example---post-and-get-endpoints)\n    - [1. Create SQLAlchemy model](#1-create-sqlalchemy-model)\n    - [2. Create and apply alembic migration](#2-create-and-apply-alembic-migration)\n    - [3. Create request and response schemas](#3-create-request-and-response-schemas)\n    - [4. Create endpoints](#4-create-endpoints)\n    - [5. Write tests](#5-write-tests)\n  - [Design](#design)\n    - [Deployment strategies - via Docker image](#deployment-strategies---via-docker-image)\n    - [Docs URL, CORS and Allowed Hosts](#docs-url-cors-and-allowed-hosts)\n  - [License](#license)\n\n\n## Features\n\n- [x] Template repository\n- [x] SQLAlchemy 2.0, async queries, best possible autocompletion support\n- [x] PostgreSQL 16 database under `asyncpg`, docker-compose.yml\n- [x] Full [Alembic](https://alembic.sqlalchemy.org/en/latest/) migrations setup\n- [x] Refresh token endpoint (not only access like in official template)\n- [x] Ready to go Dockerfile with [uvicorn](https://www.uvicorn.org/) webserver as an example\n- [x] [Poetry](https://python-poetry.org/docs/), `mypy`, `pre-commit` hooks with [ruff](https://github.com/astral-sh/ruff)\n- [x] Perfect pytest asynchronous test setup with +40 tests and full coverage\n\n\u003cbr\u003e\n\n\n\n\u003ckbd\u003e![template-fastapi-minimal-openapi-example](https://drive.google.com/uc?export=view\u0026id=1rIXFJK8VyVrV7v4qgtPFryDd5FQrb4gr)\u003c/kbd\u003e\n\n\n\n## Quickstart\n\n### 1. Create repository from a template\n\nSee [docs](https://docs.github.com/en/repositories/creating-and-managing-repositories/creating-a-repository-from-a-template).\n\n### 2. Install dependecies with [Poetry](https://python-poetry.org/docs/)\n\n```bash\ncd your_project_name\n\n### Poetry install (python3.13)\npoetry install\n```\n\nNote, be sure to use `python3.13` with this template with either poetry or standard venv \u0026 pip, if you need to stick to some earlier python version, you should adapt it yourself (remove new versions specific syntax for example `str | int` for python \u003c 3.10)\n\n### 3. Setup database and migrations\n\n```bash\n### Setup database\ndocker-compose up -d\n\n### Run Alembic migrations\nalembic upgrade head\n```\n\n### 4. Now you can run app\n\n```bash\n### And this is it:\nuvicorn app.main:app --reload\n\n```\n\nYou should then use `git init` (if needed) to initialize git repository and access OpenAPI spec at http://localhost:8000/ by default. To customize docs url, cors and allowed hosts settings, read [section about it](#docs-url-cors-and-allowed-hosts).\n\n### 5. Activate pre-commit\n\n[pre-commit](https://pre-commit.com/) is de facto standard now for pre push activities like isort or black or its nowadays replacement ruff.\n\nRefer to `.pre-commit-config.yaml` file to see my current opinionated choices.\n\n```bash\n# Install pre-commit\npre-commit install --install-hooks\n\n# Run on all files\npre-commit run --all-files\n```\n\n### 6. Running tests\n\nNote, it will create databases for session and run tests in many processes by default (using pytest-xdist) to speed up execution, based on how many CPU are available in environment.\n\nFor more details about initial database setup, see logic `app/tests/conftest.py` file, `fixture_setup_new_test_database` function.\n\nMoreover, there is coverage pytest plugin with required code coverage level 100%.\n\n```bash\n# see all pytest configuration flags in pyproject.toml\npytest\n```\n\n\u003cbr\u003e\n\n## About\n\nThis project is heavily based on the official template https://github.com/tiangolo/full-stack-fastapi-postgresql (and on my previous work: [link1](https://github.com/rafsaf/fastapi-plan), [link2](https://github.com/rafsaf/docker-fastapi-projects)), but as it now not too much up-to-date, it is much easier to create new one than change official. I didn't like some of conventions over there also (`crud` and `db` folders for example or `schemas` with bunch of files). This template aims to be as much up-to-date as possible, using only newest python versions and libraries versions.\n\n`2.0` style SQLAlchemy API is good enough so there is no need to write everything in `crud` and waste our time... The `core` folder was also rewritten. There is great base for writting tests in `tests`, but I didn't want to write hundreds of them, I noticed that usually after changes in the structure of the project, auto tests are useless and you have to write them from scratch anyway (delete old ones...), hence less than more. Similarly with the `User` model, it is very modest, with just `id` (uuid), `email` and `password_hash`, because it will be adapted to the project anyway.\n\n2024 update:\n\nThe template was adpoted to my current style and knowledge, the test based expanded to cover more, added mypy, ruff and test setup was completly rewritten to have three things:\n\n- run test in paraller in many processes for speed \n- transactions rollback after every test\n- create test databases instead of having another in docker-compose.yml\n\n\u003cbr\u003e\n\n## Step by step example - POST and GET endpoints\n\nI always enjoy to have some kind of an example in templates (even if I don't like it much, _some_ parts may be useful and save my time...), so let's create two example endpoints:\n\n- `POST` endpoint `/pets/create` for creating `Pets` with relation to currently logged `User`\n- `GET` endpoint `/pets/me` for fetching all user's pets.\n\n\u003cbr\u003e\n\n### 1. Create SQLAlchemy model\n\nWe will add `Pet` model to `app/models.py`.\n\n```python\n# app/models.py\n\n(...)\n\nclass Pet(Base):\n    __tablename__ = \"pet\"\n\n    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)\n    user_id: Mapped[str] = mapped_column(\n        ForeignKey(\"user_account.user_id\", ondelete=\"CASCADE\"),\n    )\n    pet_name: Mapped[str] = mapped_column(String(50), nullable=False)\n\n```\n\nNote, we are using super powerful SQLAlchemy feature here - Mapped and mapped_column were first introduced in SQLAlchemy 2.0, if this syntax is new for you, read carefully \"what's new\" part of documentation https://docs.sqlalchemy.org/en/20/changelog/whatsnew_20.html.\n\n\u003cbr\u003e\n\n### 2. Create and apply alembic migration\n\n```bash\n### Use below commands in root folder in virtualenv ###\n\n# if you see FAILED: Target database is not up to date.\n# first use alembic upgrade head\n\n# Create migration with alembic revision\nalembic revision --autogenerate -m \"create_pet_model\"\n\n\n# File similar to \"2022050949_create_pet_model_44b7b689ea5f.py\" should appear in `/alembic/versions` folder\n\n\n# Apply migration using alembic upgrade\nalembic upgrade head\n\n# (...)\n# INFO  [alembic.runtime.migration] Running upgrade d1252175c146 -\u003e 44b7b689ea5f, create_pet_model\n```\n\nPS. Note, alembic is configured in a way that it work with async setup and also detects specific column changes if using `--autogenerate` flag.\n\n\u003cbr\u003e\n\n### 3. Create request and response schemas\n\nThere are only 2 files: `requests.py` and `responses.py` in `schemas` folder and I would keep it that way even for few dozen of endpoints. Not to mention this is opinionated.\n\n```python\n# app/schemas/requests.py\n\n(...)\n\n\nclass PetCreateRequest(BaseRequest):\n    pet_name: str\n\n```\n\n```python\n# app/schemas/responses.py\n\n(...)\n\n\nclass PetResponse(BaseResponse):\n    id: int\n    pet_name: str\n    user_id: str\n\n```\n\n\u003cbr\u003e\n\n### 4. Create endpoints\n\n```python\n# app/api/endpoints/pets.py\n\nfrom fastapi import APIRouter, Depends, status\nfrom sqlalchemy import select\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.api import deps\nfrom app.models import Pet, User\nfrom app.schemas.requests import PetCreateRequest\nfrom app.schemas.responses import PetResponse\n\nrouter = APIRouter()\n\n\n@router.post(\n    \"/create\",\n    response_model=PetResponse,\n    status_code=status.HTTP_201_CREATED,\n    description=\"Creates new pet. Only for logged users.\",\n)\nasync def create_new_pet(\n    data: PetCreateRequest,\n    session: AsyncSession = Depends(deps.get_session),\n    current_user: User = Depends(deps.get_current_user),\n) -\u003e Pet:\n    new_pet = Pet(user_id=current_user.user_id, pet_name=data.pet_name)\n\n    session.add(new_pet)\n    await session.commit()\n\n    return new_pet\n\n\n@router.get(\n    \"/me\",\n    response_model=list[PetResponse],\n    status_code=status.HTTP_200_OK,\n    description=\"Get list of pets for currently logged user.\",\n)\nasync def get_all_my_pets(\n    session: AsyncSession = Depends(deps.get_session),\n    current_user: User = Depends(deps.get_current_user),\n) -\u003e list[Pet]:\n    pets = await session.scalars(\n        select(Pet).where(Pet.user_id == current_user.user_id).order_by(Pet.pet_name)\n    )\n\n    return list(pets.all())\n\n```\n\nAlso, we need to add newly created endpoints to router.\n\n```python\n# app/api/api.py\n\n(...)\n\nfrom app.api.endpoints import auth, pets, users\n\n(...)\n\napi_router.include_router(pets.router, prefix=\"/pets\", tags=[\"pets\"])\n\n```\n\n\u003cbr\u003e\n\n### 5. Write tests\n\nWe will write two really simple tests in combined file inside newly created `app/tests/test_pets` folder.\n\n```python\n# app/tests/test_pets/test_pets.py\n\nfrom fastapi import status\nfrom httpx import AsyncClient\nfrom sqlalchemy.ext.asyncio import AsyncSession\n\nfrom app.main import app\nfrom app.models import Pet, User\n\n\nasync def test_create_new_pet(\n    client: AsyncClient, default_user_headers: dict[str, str], default_user: User\n) -\u003e None:\n    response = await client.post(\n        app.url_path_for(\"create_new_pet\"),\n        headers=default_user_headers,\n        json={\"pet_name\": \"Tadeusz\"},\n    )\n    assert response.status_code == status.HTTP_201_CREATED\n\n    result = response.json()\n    assert result[\"user_id\"] == default_user.user_id\n    assert result[\"pet_name\"] == \"Tadeusz\"\n\n\nasync def test_get_all_my_pets(\n    client: AsyncClient,\n    default_user_headers: dict[str, str],\n    default_user: User,\n    session: AsyncSession,\n) -\u003e None:\n    pet1 = Pet(user_id=default_user.user_id, pet_name=\"Pet_1\")\n    pet2 = Pet(user_id=default_user.user_id, pet_name=\"Pet_2\")\n\n    session.add(pet1)\n    session.add(pet2)\n    await session.commit()\n\n    response = await client.get(\n        app.url_path_for(\"get_all_my_pets\"),\n        headers=default_user_headers,\n    )\n    assert response.status_code == status.HTTP_200_OK\n\n    assert response.json() == [\n        {\n            \"user_id\": pet1.user_id,\n            \"pet_name\": pet1.pet_name,\n            \"id\": pet1.id,\n        },\n        {\n            \"user_id\": pet2.user_id,\n            \"pet_name\": pet2.pet_name,\n            \"id\": pet2.id,\n        },\n    ]\n\n\n```\n\n## Design\n\n### Deployment strategies - via Docker image\n\nThis template has by default included `Dockerfile` with [Uvicorn](https://www.uvicorn.org/) webserver, because it's simple and just for showcase purposes, with direct relation to FastAPI and great ease of configuration. You should be able to run container(s) (over :8000 port) and then need to setup the proxy, loadbalancer, with https enbaled, so the app stays behind it.\n\nIf you prefer other webservers for FastAPI, check out [Nginx Unit](https://unit.nginx.org/), [Daphne](https://github.com/django/daphne), [Hypercorn](https://pgjones.gitlab.io/hypercorn/index.html).\n\n### Docs URL, CORS and Allowed Hosts\n\nThere are some **opinionated** default settings in `/app/main.py` for documentation, CORS and allowed hosts.\n\n1. Docs\n\n    ```python\n    app = FastAPI(\n        title=\"minimal fastapi postgres template\",\n        version=\"6.1.0\",\n        description=\"https://github.com/rafsaf/minimal-fastapi-postgres-template\",\n        openapi_url=\"/openapi.json\",\n        docs_url=\"/\",\n    )\n    ```\n\n   Docs page is simpy `/` (by default in FastAPI it is `/docs`). You can change it completely for the project, just as title, version, etc.\n\n2. CORS\n\n    ```python\n    app.add_middleware(\n        CORSMiddleware,\n        allow_origins=[str(origin) for origin in config.settings.BACKEND_CORS_ORIGINS],\n        allow_credentials=True,\n        allow_methods=[\"*\"],\n        allow_headers=[\"*\"],\n    )\n    ```\n\n   If you are not sure what are CORS for, follow https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS. React and most frontend frameworks nowadays operate on `http://localhost:3000` thats why it's included in `BACKEND_CORS_ORIGINS` in .env file, before going production be sure to include your frontend domain here, like `https://my-fontend-app.example.com`.\n\n3. Allowed Hosts\n\n   ```python\n   app.add_middleware(TrustedHostMiddleware, allowed_hosts=config.settings.ALLOWED_HOSTS)\n   ```\n\n   Prevents HTTP Host Headers attack, you shoud put here you server IP or (preferably) full domain under it's accessible like `example.com`. By default in .env there are two most popular records: `ALLOWED_HOSTS=[\"localhost\", \"127.0.0.1\"]`\n\n\n## License\n\nThe code is under MIT License. It's here for educational purposes, created mainly to have a place where up-to-date Python and FastAPI software lives. Do whatever you want with this code.","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frafsaf%2Fminimal-fastapi-postgres-template","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frafsaf%2Fminimal-fastapi-postgres-template","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frafsaf%2Fminimal-fastapi-postgres-template/lists"}