Od zera do deployu: aplikacja FastAPI z testami i CI w 60 minut

1
66
4/5 - (1 vote)

Nawigacja:

Założenia projektu i efekt po 60 minutach

Co dokładnie ma powstać i dlaczego właśnie taki projekt

Celem jest zbudowanie małego, ale kompletnego backendu HTTP w FastAPI: prostego API typu „to-do / notes” z kilkoma endpointami. Aplikacja ma pozwalać na:

  • utworzenie notatki / zadania (POST),
  • pobranie listy wszystkich notatek (GET),
  • pobranie pojedynczej notatki po ID (GET),
  • usunięcie notatki po ID (DELETE).

Logika biznesowa będzie maksymalnie prosta, bez bazy danych – dane przechowywane w pamięci (lista w Pythonie). Chodzi o to, żeby skupić się na:

  • strukturze projektu FastAPI,
  • podstawowych wzorcach (routery, schematy Pydantic),
  • testach (pytest, TestClient),
  • działającym pipeline CI w GitHub Actions,
  • prostym, tanim wdrożeniu (deploy) na serwer z Uvicornem.

Zakres pracy: od gołego repo do działającego deployu

Projekt obejmuje wyłącznie backend. Nie będzie tu frontu, skomplikowanych baz danych, kolejek, cache czy mikrousług. Zakres:

  • inicjalizacja środowiska i projektu,
  • kod API w FastAPI (CRUD w pamięci),
  • testy jednostkowe i integracyjne z pytest,
  • minimalny pipeline CI w GitHub Actions,
  • deploy na tanią infrastrukturę: prosty VPS lub darmowy/niski plan PaaS.

To jest minimalny, sensowny zestaw elementów, który daje realną wartość: kod nie psuje się po każdej zmianie, pipeline CI ciągle odpala testy, a aplikacja jest dostępna z zewnątrz pod URL-em.

Stack technologiczny „za grosze”

Żeby utrzymać koszty i czas w ryzach, stosujemy:

  • Python – stabilna wersja 3.10 lub 3.11,
  • FastAPI – popularny, szybki framework do API,
  • Uvicorn – lekki serwer ASGI,
  • pytest – praktyczny framework do testów,
  • Git i GitHub – repozytorium i CI,
  • GitHub Actions – darmowy pipeline CI w ramach limitów,
  • tani VPS (np. najtańszy plan u popularnego dostawcy) albo prosty PaaS (np. Railway, Render, fly.io – darmowe/tanie plany).

Żadnych drogich, enterprise’owych usług. Wszystko w wersji „budżet, ale nie drut”.

Minimalne wymagania od czytelnika

Żeby przejść całość w rozsądnym czasie, przydają się:

  • podstawowa znajomość Pythona (funkcje, moduły, typy),
  • podstawy gita (commit, push, branch nie są czarną magią),
  • konto na GitHubie,
  • umiejętność uruchomienia polecenia w terminalu.

Nie potrzeba natomiast doświadczenia w FastAPI, pytest, CI albo Dockerze – to będzie budowane po kolei od zera.

Jak zorganizować 60 minut pracy

Zamiast jednego długiego maratonu, sensowniej podzielić pracę na krótkie sprinty:

  1. 10–15 min – środowisko, wirtualne środowisko, instalacja paczek, repo git,
  2. 10–15 min – podstawowy kod FastAPI i prosty CRUD,
  3. 10–15 min – testy pytest i uruchomienie ich lokalnie,
  4. 10–15 min – konfiguracja GitHub Actions (CI),
  5. 10–15 min – przygotowanie do deployu i sam deploy.

Każdy etap daje osobny, namacalny efekt. Jeśli czas się rozjedzie, można odłożyć deploy na kolejny blok, ale kod z testami i CI już będzie mieć wartość.

Środowisko i narzędzia – tanio, szybko, wystarczająco dobrze

Wersja Pythona i FastAPI – stabilnie zamiast najnowszej zabawki

Do projektów produkcyjnych rozsądniej trzymać się wspieranych, stabilnych wersji. Dobrym kompromisem jest Python 3.10 lub 3.11 – szeroko dostępny, wspierany i bez egzotycznych problemów. FastAPI jest wstecznie kompatybilne w dużym stopniu, więc nie ma sensu gonić za absolutnie najnowszą wersją beta.

Szybki start:

  • sprawdź wersję Pythona: python --version lub python3 --version,
  • jeśli używasz systemowej wersji 3.10/3.11 – wystarczy,
  • jeśli masz starą (np. 3.7) – zainstaluj nowszą z oficjalnych paczek Pythona lub menedżera typu pyenv.

venv vs Poetry – co opłaca się na start

Do prostego API z testami i CI w 60 minut wystarczy venv + requirements.txt. Poetry daje ładniejszy zarządzanie zależnościami, ale wymaga dodatkowej nauki i konfiguracji, która na małym projekcie może być przerostem formy.

NarzędzieZaletyWadyKiedy wybrać
venv + pipWbudowany w Pythona, prosty, działa wszędzieBrak zaawansowanego zarządzania zależnościamiMałe projekty, szybki start, demo w 60 minut
PoetryLepsze zarządzanie wersjami, lockfile, dependency resolutionWiększa złożoność, dodatkowa instalacjaWiększe projekty, team, wiele zależności

Na budżetowy start komenda:

python -m venv .venv
# Linux / macOS:
source .venv/bin/activate
# Windows (PowerShell):
.venvScriptsActivate.ps1
pip install --upgrade pip
pip install fastapi uvicorn[standard] pytest

W ten sposób od razu masz lokalne środowisko odizolowane od systemowego Pythona, z kluczowymi paczkami.

Edytor / IDE – VS Code jako sensowny środek

Do FastAPI i Pythona świetnie wystarcza Visual Studio Code z darmowymi rozszerzeniami:

  • Python (oficjalne rozszerzenie),
  • Pylance (podpowiedzi, typowanie),
  • GitLens (praca z gitem),
  • REST Client lub Thunder Client (testowanie API z poziomu edytora).

Pełne IDE typu PyCharm Professional ma ogrom możliwości, ale zajmuje więcej zasobów, wymaga licencji i konfiguracji. Przy małym API różnica w efekcie nie uzasadnia nakładu – VS Code + terminal pokrywają wszystkie potrzeby.

Szybki sanity-check środowiska

Prosta checklista, czy środowisko działa:

  • aktywowane venv: w terminalu widać prefiks (.venv),
  • python -c "import fastapi, pytest, uvicorn; print('OK')" działa bez błędów,
  • git --version oraz pytest --version zwracają sensowne wartości.

Jeśli któryś krok sypie błędami, lepiej zatrzymać się i naprawić to teraz, niż walczyć z tym przy deployu.

Alternatywy: Conda, pyenv i kiedy odpuścić

Conda i pyenv przydają się przy wielu projektach, wymaganiach typu „różne wersje Pythona” albo w środowiskach data-science. Do jednego, prostego API bez ciężkich zależności:

  • Conda jest często zbyt ciężka,
  • pyenv fajnie rozwiązuje wersje Pythona, ale wymaga dodatkowej konfiguracji.

Jeśli działasz na własnym laptopie z aktualnym Pythonem – venv w zupełności wystarczy i oszczędza czas.

Zbliżenie kolorowego kodu programistycznego na ekranie monitora
Źródło: Pexels | Autor: Markus Spiske

Inicjalizacja projektu – struktura na teraz, która nie przeszkodzi jutro

Minimalna, ale sensowna struktura katalogów

Przy małym projekcie nie ma sensu tworzyć 10 katalogów i 30 modułów. Wystarczy:

fastapi-notes/
├── app/
│   ├── __init__.py
│   ├── main.py
│   ├── routers/
│   │   ├── __init__.py
│   │   └── notes.py
│   └── schemas.py
├── tests/
│   └── test_notes.py
├── requirements.txt
├── .gitignore
└── README.md

Taka struktura jest lekka, a jednocześnie pozwala dołożyć kolejne routery, schematy albo moduły bez burzenia wszystkiego.

Pliki startowe: main.py, requirements.txt, .gitignore

Na początek stwórz katalog projektu i pliki:

mkdir fastapi-notes
cd fastapi-notes
mkdir app app/routers tests
touch app/__init__.py app/main.py app/routers/__init__.py app/routers/notes.py app/schemas.py tests/test_notes.py requirements.txt .gitignore README.md

W requirements.txt na starcie możesz wpisać ręcznie tylko kluczowe zależności:

fastapi
uvicorn[standard]
pytest

To minimalna lista, której potrzebuje aplikacja i testy. W późniejszym etapie można wygenerować pełny plik z wersjami przez pip freeze, jeśli pojawi się potrzeba.

Plik .gitignore w wersji podstawowej:

__pycache__/
*.pyc
.venv/
.env
.idea/
.vscode/

Wzorzec modułów: routers, schemas, models w wersji „light”

Klasyczny podział w FastAPI to:

  • routers – definicje endpointów,
  • schemas – modele Pydantic (request/response),
  • models – modele bazodanowe (SQLAlchemy itp.).

Ponieważ korzystamy z „bazy w pamięci”, pominiemy na razie katalog models. Wystarczy:

  • app/routers/notes.py – endpointy CRUD,
  • app/schemas.py – modele Pydantic NoteCreate, Note.

Przy przejściu na prawdziwą bazę będzie można dołożyć katalog models, bez ruszania routerów i schematów.

Konfiguracja przez zmienne środowiskowe bez ciężkich bibliotek

Na małym API nie ma sensu od razu wciągać pydantic-settings czy innych narzędzi. Konfigurację typu APP_ENV czy DEBUG można obsłużyć przez os.getenv:

# app/main.py
import os
from fastapi import FastAPI

app = FastAPI()

APP_ENV = os.getenv("APP_ENV", "local")

Przy deployu na VPS czy PaaS i tak ustawisz zmienne środowiskowe w panelu lub w pliku systemd, więc nie ma potrzeby komplikować tego na starcie.

Inicjalizacja repozytorium git i pierwszy commit

Po przygotowaniu struktury i plików od razu zainicjuj repozytorium:

git init
git add .
git commit -m "Initial FastAPI notes project structure"

Na tym etapie opłaca się od razu utworzyć repo na GitHubie (np. przez przeglądarkę) i podpiąć je:

git remote add origin git@github.com:twoj-user/fastapi-notes.git
git push -u origin main

Dzięki temu kolejne kroki (testy, CI) od razu będą zintegrowane z GitHubem, bez późniejszego przepinania.

Podstawowa aplikacja FastAPI – od „hello world” do prostego CRUD

Pierwszy endpoint GET i uruchomienie serwera

Na początek prosty endpoint typu „hello” w app/main.py:

# app/main.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/health")
def read_health():
    return {"status": "ok"}

Uruchom serwer (z katalogu głównego projektu):

uvicorn app.main:app --reload

Odwiedź w przeglądarce http://127.0.0.1:8000/health. Powinieneś zobaczyć:

{"status": "ok"}

To potwierdza, że FastAPI i Uvicorn działają prawidłowo.

Model danych z Pydantic – notatka / zadanie

Dodaj modele danych w app/schemas.py:

# app/schemas.py
from typing import Optional
from pydantic import BaseModel

class NoteCreate(BaseModel):
    title: str
    content: Optional[str] = None

class Note(NoteCreate):
    id: int

Tutaj:

  • NoteCreate – dane przy tworzeniu notatki (bez ID),
  • Prosty „storage” w pamięci – lista zamiast bazy

    Do szybkiego CRUD-u wystarczy przechowywanie danych w pamięci. To nie jest rozwiązanie produkcyjne, ale na demo, proof-of-concept albo warsztaty jest idealne – zero konfiguracji, zero kosztu.

    Uproszczony „storage” w app/routers/notes.py:

# app/routers/notes.py
from typing import List
from fastapi import APIRouter, HTTPException, status
from app.schemas import Note, NoteCreate

router = APIRouter(prefix="/notes", tags=["notes"])

# Udajemy bazę danych w pamięci
notes_db: list[Note] = []
current_id = 0

def get_next_id() -> int:
    global current_id
    current_id += 1
    return current_id

Zmienne notes_db i current_id są globalne w module. To uproszczenie, ale na małym projekcie jest czytelne i nie wymaga bawienia się w klasy repozytoriów czy ORM.

Endpoint POST – tworzenie notatki

Czas na pierwszy „prawdziwy” endpoint CRUD – dodanie notatki. Uzupełnij plik app/routers/notes.py:

@router.post("/", response_model=Note, status_code=status.HTTP_201_CREATED)
def create_note(note_in: NoteCreate) -> Note:
    new_note = Note(id=get_next_id(), **note_in.dict())
    notes_db.append(new_note)
    return new_note

Tutaj:

  • response_model=Note – FastAPI automatycznie serializuje odpowiedź według modelu,
  • status_code=201 – sygnał, że zasób został utworzony,
  • **note_in.dict() – przekazanie danych z modelu wejściowego do modelu wyjściowego.

Taki endpoint można od razu przetestować przez wbudowane docs.

Endpointy GET – lista i szczegół

Do sensownego CRUD-u potrzebne jest odczytanie wszystkich notatek i pojedynczej:

@router.get("/", response_model=list[Note])
def list_notes() -> list[Note]:
    return notes_db

@router.get("/{note_id}", response_model=Note)
def get_note(note_id: int) -> Note:
    for note in notes_db:
        if note.id == note_id:
            return note
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="Note not found",
    )

Zamiast kombinować z filtrowaniem, używana jest prosta pętla – jest wystarczająco szybka dla kilkunastu czy kilkudziesięciu rekordów. Przy prawdziwej bazie i tak logika zmieni się na zapytania SQL lub ORM.

Endpointy PUT i DELETE – aktualizacja oraz usuwanie

Na małe API wystarczy jeden typ aktualizacji – pełne nadpisanie notatki (PUT). Później można dołożyć PATCH, jeśli będzie potrzeba.

@router.put("/{note_id}", response_model=Note)
def update_note(note_id: int, note_in: NoteCreate) -> Note:
    for idx, note in enumerate(notes_db):
        if note.id == note_id:
            updated_note = Note(id=note.id, **note_in.dict())
            notes_db[idx] = updated_note
            return updated_note
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="Note not found",
    )

@router.delete("/{note_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_note(note_id: int) -> None:
    for idx, note in enumerate(notes_db):
        if note.id == note_id:
            del notes_db[idx]
            return
    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="Note not found",
    )

Zamiast wymyślnego repozytorium – zwykła lista i indeks. Kluczowe jest jedynie to, żeby API zachowywało się przewidywalnie: 404 przy braku notatki, 204 przy poprawnym usunięciu.

Podpięcie routera w main.py i wbudowane Swagger UI

Sam router nie wystarczy – trzeba go podłączyć do aplikacji. W app/main.py:

# app/main.py
from fastapi import FastAPI
from app.routers import notes

app = FastAPI(title="FastAPI Notes")

app.include_router(notes.router)

@app.get("/health")
def read_health():
    return {"status": "ok"}

Po uruchomieniu:

uvicorn app.main:app --reload

Można wejść na http://127.0.0.1:8000/docs i „klikać” CRUD przez Swagger UI, bez żadnych dodatkowych narzędzi. To darmowy, wbudowany panel – szkoda go nie użyć, zamiast od razu kupować czy konfigurować zewnętrzne rozwiązania.

Minimalne porządki w zależnościach – blokowanie wersji tanim kosztem

Żeby uniknąć niespodzianek po kilku tygodniach (np. nowa wersja biblioteki psuje CI), dobrze jest szybko zablokować wersje zależności.

Najprostszy wariant, bez Poetriego i bez zewnętrznych narzędzi:

pip freeze > requirements.lock.txt

Plik requirements.txt zostaje krótki (ręcznie utrzymywany), a requirements.lock.txt zawiera dokładne wersje, z których korzystasz lokalnie. W CI można instalować z locka:

pip install -r requirements.lock.txt

To tani kompromis między pełnym zarządzaniem zależnościami a chaosem wersji „latest”.

Testy jednostkowe – pierwszy test „na żywo” z TestClient

FastAPI ma wbudowany, lekki sposób testowania endpointów przez TestClient (bazujący na requests). Do prostych testów nie potrzeba nawet uruchamiać serwera.

Podstawowy test zdrowia w tests/test_notes.py:

# tests/test_notes.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_health():
    response = client.get("/health")
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}

Uruchomienie:

pytest

Kiedy ten test przechodzi, wiadomo, że aplikacja przynajmniej startuje i podstawowy endpoint działa. To nic nie kosztuje, a potrafi szybko wychwycić błędy typu „zapomniałem importu”.

Testy CRUD – scenariusz od utworzenia do usunięcia

Zamiast 10 mikro-testów, na start wystarczy jeden scenariusz, który przechodzi przez główne operacje. To dobry kompromis między pokryciem a czasem pisania.

def test_notes_crud_flow():
    # 1. Lista początkowo pusta
    response = client.get("/notes/")
    assert response.status_code == 200
    assert response.json() == []

    # 2. Tworzymy notatkę
    payload = {"title": "Test note", "content": "Hello"}
    response = client.post("/notes/", json=payload)
    assert response.status_code == 201
    data = response.json()
    assert data["id"] > 0
    assert data["title"] == payload["title"]
    note_id = data["id"]

    # 3. Pobieramy szczegóły
    response = client.get(f"/notes/{note_id}")
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == note_id
    assert data["content"] == "Hello"

    # 4. Aktualizujemy notatkę
    updated = {"title": "Updated", "content": "World"}
    response = client.put(f"/notes/{note_id}", json=updated)
    assert response.status_code == 200
    data = response.json()
    assert data["title"] == "Updated"
    assert data["content"] == "World"

    # 5. Usuwamy
    response = client.delete(f"/notes/{note_id}")
    assert response.status_code == 204

    # 6. Sprawdzamy, że już jej nie ma
    response = client.get(f"/notes/{note_id}")
    assert response.status_code == 404

Taki test pachnie integracyjnym, ale przy tak prostej aplikacji to nie problem. Jedna funkcja pokrywa główny przepływ użytkownika, a czas wykonania nadal jest minimalny.

Reset stanu między testami – proste „clearowanie” pamięci

Przy „bazie” w pamięci kolejne testy mogą na siebie wpływać. Można to rozwiązać bardzo tanim sposobem – prostą funkcją pomocniczą i fiksturą pytest.

Najpierw w app/routers/notes.py dodaj funkcję do czyszczenia danych:

def reset_notes():
    global notes_db, current_id
    notes_db = []
    current_id = 0

Następnie w tests/conftest.py (trzeba utworzyć plik) dodaj fiksturę:

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.routers.notes import reset_notes

@pytest.fixture(autouse=True)
def client():
    # Resetujemy stan „bazy” przed każdym testem
    reset_notes()
    return TestClient(app)

Teraz testy mogą używać fikstury client jako argumentu funkcji:

def test_health(client):
    response = client.get("/health")
    assert response.status_code == 200

Autouse zapewnia, że stan pamięci resetuje się przed każdym testem, bez ręcznego wołania. Efekt: brak losowych błędów w stylu „test zależny od kolejności uruchamiania”.

Oddzielenie testów „szybkich” od „ciężkich” przez markery

Na starcie wszystkie testy są lekkie, ale dobrze zbudować nawyk oznaczania cięższych scenariuszy. Później, gdy dojdzie prawdziwa baza albo zewnętrzne API, taka separacja oszczędzi czas w CI.

Prosty przykład z markerem:

# tests/test_notes.py
import pytest

@pytest.mark.integration
def test_notes_crud_flow(client):
    ...

Wtedy lokalnie można odpalać szybkie testy:

pytest -m "not integration"

A pełen komplet zostawić na push do GitHuba. To nie wymaga żadnych dodatkowych narzędzi, tylko rozsądnej konwencji.

Podstawowy workflow GitHub Actions – testy na każdym pushu

CI w GitHub Actions ma darmowy pakiet minut dla publicznych repozytoriów i rozsądny limit dla prywatnych. Do małego API to wystarcza z dużym zapasem, więc nie ma sensu szukać płatnych alternatyw na start.

Struktura katalogu:

fastapi-notes/
└── .github/
    └── workflows/
        └── ci.yml

Treść minimalnego workflow .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: ["main"]
  pull_request:
    branches: ["main"]

jobs:
  tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          if [ -f requirements.lock.txt ]; then pip install -r requirements.lock.txt; else pip install -r requirements.txt; fi

      - name: Run tests
        run: |
          pytest -m "not integration" --maxfail=1 --disable-warnings -q

Warunek z requirements.lock.txt pozwala używać locka, jeśli istnieje, ale nie wymusza go od pierwszego commita. To kolejny mały „budżetowy” trik, który nie komplikuje startu.

Szybka walidacja pipeline’u – lokalny „suchy” bieg

Zanim commit trafi na GitHuba, łatwo sprawdzić, czy workflow ma sens:

  • ta sama wersja Pythona lokalnie, co w CI (np. 3.11),
  • instalacja zależności przez pip install -r requirements.txt na świeżym venv,
  • uruchomienie pytest -m "not integration".

Jeśli wszystko przechodzi na „czystym” środowisku, CI raczej też będzie zielone. Najczęstszy błąd to ukryte zależności zainstalowane globalnie – świeży venv od razu to demaskuje, bez tracenia czasu na zgadywanie, co się dzieje na serwerze GitHuba.

Dodanie badge’a statusu CI do README – motywacja za zero złotych

Kiedy pipeline już działa, można dodać prosty badge do README.md. To żaden wymóg, ale wizualny sygnał pomaga utrzymać dyscyplinę commitów „na zielono”.

Przykładowy fragment markdown (dopasuj nazwę usera i repo):

[![CI](https://github.com/twoj-user/fastapi-notes/actions/workflows/ci.yml/badge.svg)](https://github.com/twoj-user/fastapi-notes/actions/workflows/ci.yml)

Koszt dodania – minuta, efekt – szybka informacja, czy ostatnie zmiany przechodzą testy, bez wchodzenia w zakładkę Actions.

Najprostszy deploy „na dziś” – uvicorn + systemd na tanim VPS

Zamiast rozbudowanych PaaS czy Kubernetesów, na pojedyncze API w małej skali wystarczy tani VPS za kilka dolarów i zwykły uvicorn za reverse proxy (np. Nginx). To wariant, który nie zabije rachunku.

Minimalny szkic kroków (bez wchodzenia w każdy szczegół systemowy):

  1. Na VPS:
    • zainstaluj Pythona 3.11,
    • sklonuj repo: git clone ...,
    • utwórz venv i zainstaluj zależności.
  2. Utwórz plik jednostki systemd, np. /etc/systemd/system/fastapi-notes.service:
    [Unit]
    Description=FastAPI Notes
    After=network.target
    
    [Service]
    User=www-data
    WorkingDirectory=/opt/fastapi-notes
    Environment="APP_ENV=prod"
    ExecStart=/opt/fastapi-notes/.venv/bin/uvicorn app.main:app --host 0.0.0.0 --port 8000
    Restart=always
    
    [Install]
    WantedBy=multi-user.target
    
  3. Prosty Nginx jako reverse proxy – HTTPS bez drogich cudów

    Uvicorn może serwować ruch bezpośrednio, ale lepiej postawić przed nim prosty reverse proxy. Dwie główne korzyści:

  • HTTPS za darmo z Let’s Encrypt,
  • stabilniejsza obsługa połączeń i statycznych zasobów (logi, time-outy, limity).

Na budżetowym VPS sprawdza się klasyczny Nginx. Minimalna konfiguracja (dla domeny notes.example.com) w pliku, np. /etc/nginx/sites-available/fastapi-notes:

server {
    listen 80;
    server_name notes.example.com;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

Aktywacja:

ln -s /etc/nginx/sites-available/fastapi-notes /etc/nginx/sites-enabled/fastapi-notes
nginx -t
systemctl reload nginx

Do tego darmowy certyfikat TLS z certbot:

apt install -y certbot python3-certbot-nginx
certbot --nginx -d notes.example.com

Po tym kroku ruch przechodzi po HTTPS, a Nginx przekazuje żądania do uvicorna na porcie 8000. Bez Kubernetesa, bez Traefika, bez faktur na kilkaset zł miesięcznie.

Ręczny deploy z Git pull – pół godziny konfiguracji, później chwila pracy

Na jedno małe API w zupełności wystarczy najprostszy sposób wdrożenia: git pull na serwerze i restart usługi. Zero dodatkowych usług, zero abonamentów.

Na VPS repozytorium może żyć w katalogu, np. /opt/fastapi-notes. Typowy cykl:

cd /opt/fastapi-notes
git pull origin main
source .venv/bin/activate
pip install -r requirements.txt
systemctl restart fastapi-notes

Taki workflow jest „manualny”, ale przy małej częstotliwości deployów (np. raz na tydzień) jest po prostu najtańszy czasowo i finansowo. Zwykle dopiero przy rosnącym zespole i kilku środowiskach sens ma inwestycja w pełne CD.

Jeśli z założenia deploy ma być ultra-ostrożny, można wprowadzić prostą kontrolę:

  • zmiany na main tylko przez PR z zielonym CI,
  • deploy robiony ze świeżego commita z GH (sprawdzenie git log -1),
  • krótki smoke-test tuż po restarcie: curl http://localhost:8000/health.

To nadal jest budżetowy model, ale ryzyko psucia produkcji jest już całkiem sensownie ograniczone.

Najprostszy rollback – jeden dodatkowy branch zamiast rozbudowanego release managementu

Żeby zjechać z wersji w dół, nie trzeba od razu całej orkiestracji. W małym projekcie wystarczy lekka konwencja na gicie.

  1. Po każdym udanym deployu taguj commit:
    git tag -a deploy-2024-01-18 -m "Deploy 2024-01-18"
    git push origin deploy-2024-01-18
    
  2. W razie problemu najprostszy rollback:
    git checkout deploy-2024-01-18
    systemctl restart fastapi-notes
    

Jeżeli nie ma potrzeby deployowania kilka razy dziennie, takie ręczne wycofanie w razie kryzysu w zupełności wystarczy. Czas odzyskania działania usługi liczy się w minutach, a implementacja w godzinach nie istnieje – tylko jeden nawyk: tag po każdym wdrożeniu.

Budżetowe logowanie – proste logi uvicorna zamiast pełnego stacka obserwowalności

Zanim pojawi się ruch na poziomie setek żądań na sekundę, koszt stawiania pełnego ELK czy Prometheusa zwyczajnie się nie spina. Początkowo wystarczy:

  • logowanie uvicorna do pliku,
  • obrót logów przez system narzędzi typu logrotate.

W pliku jednostki systemd logi lecą domyślnie do journalctl. Jeśli wygodniej mieć plik, można użyć przekierowania:

[Service]
...
ExecStart=/opt/fastapi-notes/.venv/bin/uvicorn app.main:app 
  --host 0.0.0.0 --port 8000 
  --log-config logging.ini

Plik logging.ini może kierować logi do prostego pliku tekstowego. Na start często wystarcza zaawansowany „stack” w postaci:

  • journalctl -u fastapi-notes -f przy debugowaniu,
  • od czasu do czasu prosty grep po logach w /var/log.

To nie jest kompromis na wieczność, ale przy pierwszych iteracjach pomaga skupić się na funkcjach zamiast na infrastrukturze logowania.

Zbliżenie ekranu z kodem Ruby on Rails podkreślającym złożoność aplikacji
Źródło: Pexels | Autor: Digital Buggu

Automatyzacja prostego deployu z GitHub Actions – tani pseudo-CD

Ręczny deploy jest w porządku, dopóki zmian jest mało. Gdy commitów przybywa, dobrze przerzucić powtarzalne kroki na automat. Da się to zrobić przy użyciu tego, co już jest – GitHub Actions – i prostego dostępu SSH.

Dostęp bez hasła – klucz SSH do deployu

Do zdalnego deployu z GH Actions potrzebny jest bezpieczny dostęp do serwera. Najtańsza opcja:

  1. Na lokalnej maszynie (albo na serwerze) wygeneruj dedykowany klucz:
    ssh-keygen -t ed25519 -C "ci-deploy" -f ci-deploy-key
    
  2. Publiczny klucz (ci-deploy-key.pub) dodaj do ~/.ssh/authorized_keys użytkownika używanego do deployu, np. deploy.
  3. Prywatny klucz (ci-deploy-key) zaszyfruj jako sekret w repozytorium GitHuba, np. DEPLOY_SSH_KEY.

Dzięki temu workflow będzie mógł zalogować się na serwer jak każdy zwykły użytkownik, bez otwierania dodatkowych portów i kombinowania z VPN-ami.

Osobny workflow na deploy – prosty i czytelny podział

Jedna z prostszych konfiguracji to rozdzielenie CI (testy) i CD (deploy) na dwa workflowy. Deploy niech uruchamia się ręcznie albo tylko na tagach.

Struktura:

.github/
  workflows/
    ci.yml
    deploy.yml

Przykładowa treść .github/workflows/deploy.yml:

name: Deploy

on:
  workflow_dispatch:
  push:
    tags:
      - "deploy-*"

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repo
        uses: actions/checkout@v4

      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }}

      - name: Deploy to server
        run: |
          ssh -o StrictHostKeyChecking=no deploy@your-server-ip << 'EOF'
          set -e
          cd /opt/fastapi-notes
          git fetch origin
          git checkout main
          git pull origin main
          source .venv/bin/activate
          pip install -r requirements.txt
          pytest -m "not integration" --maxfail=1 --disable-warnings -q
          systemctl restart fastapi-notes
          EOF

Koszt wdrożenia takiego pseudo-CD to zwykle 1–2 godziny. Zysk: jeden przycisk „Run workflow” albo prosty tag git tag deploy-2024-01-18 && git push --tags, który spina build, testy i restart serwisu.

Bezpieczny warunek na deploy – tylko zielony CI i tylko z main

Żeby nie zrobić sobie krzywdy jednym nieuważnym tagiem, wystarczy kilka prostych reguł:

  • deployowy workflow uruchamiany tylko z brancha main lub tylko na tagach,
  • branch main zabezpieczony w GitHubie (wymagane zielone testy przed merge),
  • brak bezpośrednich pushy na main – tylko PR.

Technicznie można dodać w workflow prosty check:

- name: Ensure on main
  run: |
    branch=$(git rev-parse --abbrev-ref HEAD)
    if [ "$branch" != "main" ]; then
      echo "Deploy only from main, current: $branch"
      exit 1
    fi

Taki mini-guardrail eliminuje typowe pomyłki, a przy tym nie wymaga budowania całego systemu release managementu.

Rozsądne rozszerzenia: konfiguracja i prosta „wielostage’owość” środowisk

Gdy API staje się choć trochę poważniejsze, zaczyna brakować jednej ważnej rzeczy: sensownej konfiguracji per środowisko. Można to ogarnąć w prosty, tani sposób, bez dociągania całych frameworków konfiguracyjnych.

Konfiguracja przez zmienne środowiskowe i prosty pydantic Settings

Dobry kompromis między wygodą a prostotą to:

  • ważne dane (hasła, klucze) tylko w zmiennych środowiskowych,
  • reszta w lekkich domyślnych ustawieniach w kodzie.

W FastAPI wygodnie robi się to przez Pydantic Settings. Na przykład w app/config.py:

from functools import lru_cache
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    app_env: str = "dev"
    debug: bool = True
    database_url: str = "sqlite:///./notes.db"

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

@lru_cache
def get_settings() -> Settings:
    return Settings()

W app/main.py:

from fastapi import FastAPI
from .config import get_settings

settings = get_settings()
app = FastAPI(debug=settings.debug)

Dzięki env_file lokalnie można używać zwykłego pliku .env, a na produkcji podać te same wartości przez systemd:

[Service]
Environment="APP_ENV=prod"
Environment="DEBUG=false"
Environment="DATABASE_URL=postgresql://..."

Taki układ nie rozwiązuje wszystkich problemów świata, ale pozwala rozróżnić dev, staging i prod bez budowania osobnych gałęzi kodu.

Lekki podział na środowiska – trzy pliki .env zamiast trzech klastrów

Zamiast osobnego VPS-a na każde środowisko można długo jechać na wariancie:

  • lokalne .env.development,
  • proste .env.staging na małym VPS-ie do testów,
  • zmienne środowiskowe na produkcji bez plików .env.

W projekcie wystarczy prosty przełącznik:

class Settings(BaseSettings):
    app_env: str = "dev"
    ...

    class Config:
        env_file = f".env.{os.getenv('APP_ENV', 'development')}"

Na start nawet to bywa przesadą – często wystarczy jeden VPS i osobna baza „testowa”. Chodzi o to, żeby nie duplikować całej infrastruktury, tylko mieć jedno miejsce, gdzie w razie czego można odizolować zmiany przed wejściem na produkcję.

Co dalej poprawia stosunek „efekt / wysiłek”

Gdy podstawowe klocki są na miejscu – działające API, testy, CI i prosty deploy – kolejne usprawnienia warto dobierać pod kątem najlepszej „stopy zwrotu”. Kilka elementów zwykle szybko się zwraca.

Type hints i mypy – tanie ubezpieczenie przed głupimi bugami

Przy małym kodzie statyczna analiza typów wydaje się fanaberią, ale im więcej endpointów i modeli, tym częściej wychwytuje literówki i złe założenia. W praktyce dodanie mypy to:

  1. dopisanie zależności:
    pip install mypy
    
  2. prosty config mypy.ini:
    [mypy]
    python_version = 3.11
    ignore_missing_imports = True
    disallow_untyped_defs = False
    
  3. krok w CI:
    - name: Type check
      run: mypy app
    

Na początku nie trzeba dopinać 100% typów. Sama obecność mypy potrafi wychwycić np. sytuacje, gdzie funkcja zwraca None, a kod zakłada zawsze słownik.

Formatowanie i linting – uniknięcie „wojen o styl” za pomocą jednego narzędzia

Konflikty o styl kodu potrafią zabijać czas. Zamiast dyskutować, lepiej kazać maszynie formatować za nas. Na budżetowym setupie często wystarczy:

  • black – automatyczny format,
  • ruff – szybki linter.

Instalacja:

pip install black ruff

Minimalny krok w CI:

- name: Lint with ruff
  run: ruff check app tests

- name: Format check with black
  run: black --check app tests

Lokalnie wystarczy jedna komenda przed commitem:

black app tests && ruff check app tests

Po kilku dniach przestaje się o tym myśleć, a PR-y są czytelniejsze, bo nie zawierają przypadkowych zmian w białych znakach i cudzysłowach.

Więcej testów bez przesady – pokrycie kluczowych ścieżek

Najczęściej zadawane pytania (FAQ)

Od czego zacząć naukę FastAPI, jeśli znam tylko podstawy Pythona?

Na start wystarczy zainstalować FastAPI i Uvicorn w prostym wirtualnym środowisku (venv), a potem napisać kilka najprostszych endpointów typu „hello world” i prosty CRUD. Dobrym pierwszym celem jest małe API do notatek lub zadań – kilka ścieżek, bez bazy danych, dane w liście w pamięci.

Kluczowe jest, żeby nie przytłoczyć się naraz Dockerem, Kubernetesem i skomplikowaną bazą. Lepiej zbudować jeden mały, ale kompletny backend: struktura projektu, routery, schematy Pydantic, testy pytest i prosty deploy. Dopiero gdy to działa, można dokładkać kolejne klocki.

Czy do prostego projektu w FastAPI muszę używać Dockera i bazy danych?

Nie, do małego API typu „notes / to-do” w zupełności wystarczy przechowywanie danych w pamięci (lista w Pythonie) i uruchomienie aplikacji na Uvicornie bez Dockera. To rozwiązanie nie jest produkcyjnie „wieczne”, ale świetnie nadaje się do nauki, demo, MVP czy małego wewnętrznego narzędzia.

Docker i baza (np. Postgres) mają sens, gdy API zaczyna być długożyjące, musi przetrwać restart procesu lub ma kilku użytkowników korzystających na co dzień. Na etapie 60-minutowego projektu to tylko dodatkowa konfiguracja, która spowalnia dojście do działającego URL-a.

venv czy Poetry do projektu w FastAPI – co wybrać na początek?

Do małego projektu z kilkoma zależnościami szybciej i prościej jest użyć wbudowanego venv + requirements.txt. Tworzenie środowiska to kilka komend, nie trzeba instalować dodatkowych narzędzi ani uczyć się nowego workflow.

Poetry opłaca się, gdy rośnie liczba zależności, projekt jest rozwijany w zespole albo chcesz mieć dopracowane zarządzanie wersjami i lockfile. Jeśli Twoim celem jest „postawić API z testami i CI w godzinę”, venv daje lepszy stosunek efektu do wysiłku.

Jaki minimalny stack technologiczny wystarczy do FastAPI z CI?

Do budżetowego, ale sensownego projektu wystarczy: Python 3.10 lub 3.11, FastAPI, Uvicorn jako serwer ASGI, pytest do testów, Git + GitHub jako repozytorium oraz GitHub Actions jako darmowy (w limitach) system CI. Do deployu możesz użyć taniego VPS-a albo darmowego/niskiego planu na PaaS typu Railway, Render czy fly.io.

Taki zestaw pozwala mieć pełen przepływ: od lokalnego kodu, przez testy i automatyczny pipeline, po działającą aplikację pod publicznym URL-em – bez konieczności wykupowania drogich, enterprise’owych usług.

Jaka struktura katalogów dla małego projektu FastAPI ma sens?

Przy prostym API sprawdza się układ z jednym katalogiem aplikacji i wyraźnym podziałem na routery oraz schematy Pydantic. Przykład:

  • app/main.py – start aplikacji, podpięcie routerów, konfiguracja.
  • app/routers/notes.py – endpointy CRUD do notatek.
  • app/schemas.py – modele Pydantic do requestów/response’ów.
  • tests/test_notes.py – testy jednostkowe i integracyjne pytest.

Taki układ jest mały, ale skalowalny. Jeśli projekt urośnie, możesz dodać kolejne routery (np. użytkownicy) czy moduły bez wywracania struktury do góry nogami.

Czy da się uruchomić CI dla FastAPI za darmo na GitHubie?

Tak, GitHub Actions w darmowej wersji w zupełności wystarcza do małego projektu. Wystarczy plik workflow w .github/workflows/, który na każde push lub pull request tworzy środowisko, instaluje zależności i odpala pytest.

Dla prostego API i kilku testów zużycie minut CI jest niewielkie, więc nie generuje kosztów. Dzięki temu po każdej zmianie masz automatyczną weryfikację, czy aplikacja i testy nadal działają – bez inwestowania w płatne rozwiązania CI.

Na czym najtaniej postawić prostą aplikację FastAPI z Uvicornem?

Najprostsze opcje to najtańszy plan VPS u popularnego dostawcy (kilka dolarów miesięcznie) lub darmowe/tanie PaaS jak Railway, Render, fly.io. Dla małego API często wystarczy darmowy tier, o ile nie generujesz dużego ruchu.

VPS daje więcej kontroli, ale wymaga samodzielnej konfiguracji systemu, Pythona i procesu (np. z systemd). PaaS ogranicza pracę do kilku ustawień i często prostego pliku konfiguracyjnego lub przycisku „Deploy”, więc dla pierwszego projektu stosunek pracy do efektu jest zazwyczaj korzystniejszy.

Najważniejsze wnioski

  • W 60 minut da się zbudować kompletny, mały backend HTTP w FastAPI (notes/to-do) z podstawowym CRUD-em, testami i działającym CI – bez bazy danych i skomplikowanej infrastruktury.
  • Największą wartość daje zestaw: prosta struktura projektu FastAPI, schematy Pydantic, testy (pytest + TestClient), pipeline CI w GitHub Actions i tani deploy na VPS/PaaS – zamiast rozbudowanej „enterprise” architektury.
  • Dane trzymane w pamięci (lista w Pythonie) są w zupełności wystarczające na start: pozwalają skupić się na wzorcach, testowalności i automatyzacji, a nie na konfiguracji bazy.
  • Python 3.10/3.11 + stabilna wersja FastAPI to bezpieczny, wspierany i niewydumany wybór; gonienie za najnowszymi betami nie ma sensu przy małym projekcie i ograniczonym czasie.
  • Do szybkiego startu bardziej opłaca się venv + pip + requirements.txt niż Poetry – mniej magii, prostsza konfiguracja i zero dodatkowej nauki, co przy projekcie „w godzinę” ma realne znaczenie.
  • VS Code z kilkoma darmowymi rozszerzeniami (Python, Pylance, GitLens, prosty klient REST) pokrywa wszystkie potrzeby dla takiego API; ciężkie, płatne IDE nie poprawi tu stosunku efektu do wysiłku.
  • Krótka checklista środowiska (aktywne venv, import kluczowych paczek, działający git/pytest) i podział pracy na 10–15-minutowe sprinty minimalizują ryzyko, że projekt „utknie” na drobnej konfiguracji zamiast dojść do deployu.

1 KOMENTARZ

  1. Bardzo doceniam wartościową treść artykułu „Od zera do deployu: aplikacja FastAPI z testami i CI w 60 minut”. Autor w przystępny sposób krok po kroku pokazuje, jak stworzyć kompletną aplikację w FastAPI, dodając do tego testy i CI. Szczególnie podoba mi się praktyczne podejście oraz klarowne instrukcje, które pomogły mi zrozumieć proces tworzenia aplikacji webowej.

    Jednakże, brakowało mi w artykule głębszego wyjaśnienia niektórych koncepcji, co mogłoby być pomocne dla osób, które dopiero zaczynają przygodę z FastAPI. Może warto byłoby dodać krótkie przypisy lub odnośniki do dodatkowych materiałów, które mogłyby rozwiać wątpliwości czy zagłębić się w tematykę bardziej szczegółowo.

    Mimo tej drobnej uwagi, uważam, że artykuł jest naprawdę wartościowy i polecam go wszystkim, którzy chcą nauczyć się tworzenia aplikacji w FastAPI.

Nie jesteś zalogowany — nie możesz dodać komentarza.