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:
- 10–15 min – środowisko, wirtualne środowisko, instalacja paczek, repo git,
- 10–15 min – podstawowy kod FastAPI i prosty CRUD,
- 10–15 min – testy pytest i uruchomienie ich lokalnie,
- 10–15 min – konfiguracja GitHub Actions (CI),
- 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 --versionlubpython3 --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ędzie | Zalety | Wady | Kiedy wybrać |
|---|---|---|---|
| venv + pip | Wbudowany w Pythona, prosty, działa wszędzie | Brak zaawansowanego zarządzania zależnościami | Małe projekty, szybki start, demo w 60 minut |
| Poetry | Lepsze zarządzanie wersjami, lockfile, dependency resolution | Większa złożoność, dodatkowa instalacja | Wię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 --versionorazpytest --versionzwracają 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.

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 PydanticNoteCreate,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.txtna ś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):
[](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):
- Na VPS:
- zainstaluj Pythona 3.11,
- sklonuj repo:
git clone ..., - utwórz venv i zainstaluj zależności.
- 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
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
maintylko 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.
- 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 - 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 -fprzy 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.

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:
- Na lokalnej maszynie (albo na serwerze) wygeneruj dedykowany klucz:
ssh-keygen -t ed25519 -C "ci-deploy" -f ci-deploy-key - Publiczny klucz (
ci-deploy-key.pub) dodaj do~/.ssh/authorized_keysużytkownika używanego do deployu, np.deploy. - 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
mainlub tylko na tagach, - branch
mainzabezpieczony 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.stagingna 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:
- dopisanie zależności:
pip install mypy - prosty config
mypy.ini:[mypy] python_version = 3.11 ignore_missing_imports = True disallow_untyped_defs = False - 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.







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.