Spis treści 12 sekcji
  1. Architektura naszego API
  2. Krok 1: Tworzymy tabelę DynamoDB
  3. Krok 2: Tworzymy rolę IAM dla Lambdy
  4. Krok 3: Tworzymy funkcję Lambda
  5. Krok 4: Tworzymy API Gateway
  6. Krok 5: Testujemy API
  7. Cold starty i wydajność
  8. Ile to kosztuje?
  9. Monitoring i debugowanie
  10. Zabezpieczanie API
  11. Najczęściej zadawane pytania (FAQ)
  12. Następny krok
TL;DR
  • Zbudujesz działające REST API (CRUD) z AWS Lambda, API Gateway i DynamoDB w ~30 minut
  • Lambda handler w Pythonie - pełny kod gotowy do wklejenia
  • Free Tier: 1 mln requestów Lambda + 1 mln wywołań API Gateway miesięcznie za darmo
  • Każdy krok pokazany dwoma metodami: Panel AWS i AWS CLI
  • Na końcu testujesz API za pomocą curl

Kiedy pierwszy raz usłyszałem o serverless, pomyślałem: "kolejny buzzword". Potem postawiłem swoje pierwsze API na Lambda i API Gateway - dosłownie w pół godziny miałem działający endpoint, zero serwerów do zarządzania i rachunek wynoszący 0 zł. Od tamtej pory serverless to moje domyślne podejście do małych i średnich API.

W tym artykule zbudujemy razem kompletne REST API do zarządzania notatkami. Będziemy mieli endpointy do tworzenia, odczytywania, aktualizowania i usuwania notatek (CRUD). Jako bazę danych wykorzystamy DynamoDB, a całość postawimy w ramach AWS Free Tier.

Wymagania wstępne: Konto AWS (jeśli jeszcze nie masz, sprawdź jak zacząć z AWS w 2026), podstawowa znajomość Pythona i terminala. Jeśli wybierasz metodę CLI, zainstaluj i skonfiguruj aws cli poleceniem aws configure.

Architektura naszego API

Zanim zaczniemy klikać, zobaczmy co dokładnie budujemy:

  1. API Gateway - przyjmuje requesty HTTP (GET, POST, PUT, DELETE) i przekazuje je do Lambdy
  2. Lambda - funkcja w Pythonie, która przetwarza request i komunikuje się z bazą danych
  3. DynamoDB - baza NoSQL przechowująca nasze notatki
  4. IAM Role - uprawnienia pozwalające Lambdzie czytać i zapisywać dane w DynamoDB

Klient wysyła request HTTP do API Gateway, API Gateway wywołuje Lambdę, Lambda czyta/zapisuje dane w DynamoDB i zwraca odpowiedź. Proste i skalowalne - od 1 do 10 000 requestów na sekundę bez żadnej konfiguracji.

Dwa sposoby, ten sam efekt: Każdy krok poniżej możesz wykonać na dwa sposoby - klikając w panelu webowym AWS albo wpisując komendy w terminalu. Wybierz jedną metodę i trzymaj się jej. Jeśli dopiero zaczynasz, polecam Panel AWS.

Krok 1: Tworzymy tabelę DynamoDB

Zaczynamy od bazy danych, bo Lambda będzie jej potrzebować od pierwszego uruchomienia.

  1. Otwórz konsolę AWS i przejdź do usługi DynamoDB
  2. Kliknij Create table
  3. Table name: ServerlessNotes
  4. Partition key: noteId (typ: String)
  5. Zostaw resztę ustawień domyślnie (On-demand capacity) i kliknij Create table
Screenshot: Formularz tworzenia tabeli DynamoDB z wypełnionymi polami
aws dynamodb create-table \
    --table-name ServerlessNotes \
    --attribute-definitions AttributeName=noteId,AttributeType=S \
    --key-schema AttributeName=noteId,KeyType=HASH \
    --billing-mode PAY_PER_REQUEST \
    --region eu-central-1
Dlaczego On-demand? W trybie On-demand płacisz tylko za faktyczne operacje odczytu/zapisu. Dla naszego API to idealne rozwiązanie - zero kosztów gdy nikt nie korzysta, automatyczne skalowanie przy wzroście ruchu.

Krok 2: Tworzymy rolę IAM dla Lambdy

Lambda potrzebuje uprawnień do zapisywania logów w CloudWatch oraz do operacji na tabeli DynamoDB. Zgodnie z zasadą least privilege, dajemy tylko to co niezbędne. Więcej o bezpieczeństwie IAM znajdziesz w poradniku IAM.

  1. Przejdź do IAMRolesCreate role
  2. Trusted entity: AWS service, Use case: Lambda
  3. Dodaj politykę AWSLambdaBasicExecutionRole (logi CloudWatch)
  4. Nazwij rolę: NotesApiLambdaRole i utwórz ją
  5. Otwórz utworzoną rolę, kliknij Add permissionsCreate inline policy
  6. Wybierz JSON i wklej politykę dostępu do DynamoDB (poniżej)
Screenshot: Tworzenie roli IAM z wybranym use case Lambda
# Utwórz trust policy
cat > trust-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "lambda.amazonaws.com"},
    "Action": "sts:AssumeRole"
  }]
}
EOF

# Utwórz rolę
aws iam create-role \
    --role-name NotesApiLambdaRole \
    --assume-role-policy-document file://trust-policy.json

# Dodaj politykę logowania
aws iam attach-role-policy \
    --role-name NotesApiLambdaRole \
    --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

# Dodaj inline policy dla DynamoDB
cat > dynamodb-policy.json << 'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": [
      "dynamodb:PutItem",
      "dynamodb:GetItem",
      "dynamodb:UpdateItem",
      "dynamodb:DeleteItem",
      "dynamodb:Scan"
    ],
    "Resource": "arn:aws:dynamodb:eu-central-1:TWOJE_ACCOUNT_ID:table/ServerlessNotes"
  }]
}
EOF

aws iam put-role-policy \
    --role-name NotesApiLambdaRole \
    --policy-name DynamoDBNotesAccess \
    --policy-document file://dynamodb-policy.json
Uwaga: W polityce DynamoDB zamień TWOJE_ACCOUNT_ID na rzeczywiste ID Twojego konta AWS. Znajdziesz je w prawym górnym rogu konsoli AWS. Nigdy nie używaj "Resource": "*" w produkcji.

Krok 3: Tworzymy funkcję Lambda

Czas na serce naszego API. Napiszemy jedną funkcję Lambda, która obsłuży wszystkie operacje CRUD. To podejście nazywa się "monolith Lambda" i sprawdza się świetnie przy małych API.

Kod Lambda handlera (Python 3.12)

import json
import boto3
import uuid
from datetime import datetime, timezone

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table("ServerlessNotes")

def lambda_handler(event, context):
    http_method = event["httpMethod"]
    path = event.get("pathParameters")

    try:
        if http_method == "GET" and path and path.get("noteId"):
            return get_note(path["noteId"])
        elif http_method == "GET":
            return list_notes()
        elif http_method == "POST":
            return create_note(json.loads(event["body"]))
        elif http_method == "PUT" and path:
            return update_note(path["noteId"], json.loads(event["body"]))
        elif http_method == "DELETE" and path:
            return delete_note(path["noteId"])
        else:
            return response(400, {"error": "Nieobslugiwana operacja"})
    except Exception as e:
        print(f"Blad: {str(e)}")
        return response(500, {"error": "Wewnetrzny blad serwera"})

def create_note(data):
    note = {
        "noteId": str(uuid.uuid4()),
        "title": data["title"],
        "content": data.get("content", ""),
        "createdAt": datetime.now(timezone.utc).isoformat(),
        "updatedAt": datetime.now(timezone.utc).isoformat()
    }
    table.put_item(Item=note)
    return response(201, note)

def get_note(note_id):
    result = table.get_item(Key={"noteId": note_id})
    if "Item" not in result:
        return response(404, {"error": "Notatka nie znaleziona"})
    return response(200, result["Item"])

def list_notes():
    result = table.scan()
    return response(200, result.get("Items", []))

def update_note(note_id, data):
    result = table.update_item(
        Key={"noteId": note_id},
        UpdateExpression="SET title = :t, content = :c, updatedAt = :u",
        ExpressionAttributeValues={
            ":t": data["title"],
            ":c": data.get("content", ""),
            ":u": datetime.now(timezone.utc).isoformat()
        },
        ReturnValues="ALL_NEW"
    )
    return response(200, result["Attributes"])

def delete_note(note_id):
    table.delete_item(Key={"noteId": note_id})
    return response(200, {"message": "Notatka usunieta"})

def response(status_code, body):
    return {
        "statusCode": status_code,
        "headers": {
            "Content-Type": "application/json",
            "Access-Control-Allow-Origin": "*"
        },
        "body": json.dumps(body, default=str)
    }
Wskazówka: Zwróć uwagę na nagłówek Access-Control-Allow-Origin: * w odpowiedziach. Bez niego frontend na innej domenie nie będzie mógł korzystać z API (blokada CORS). Sam nagłówek nie wystarczy: przy PUT/DELETE i JSON przeglądarka wysyła najpierw request preflight (OPTIONS), więc w API Gateway użyj opcji Enable CORS na zasobach - doda metodę OPTIONS automatycznie. W produkcji zamień * na konkretną domenę.
  1. Przejdź do LambdaCreate function
  2. Wybierz Author from scratch
  3. Function name: NotesApiHandler
  4. Runtime: Python 3.12
  5. Execution role: Use an existing roleNotesApiLambdaRole
  6. Kliknij Create function
  7. W edytorze kodu wklej cały handler powyżej i kliknij Deploy
  8. W zakładce ConfigurationGeneral configuration ustaw Timeout na 10 sekund
Screenshot: Edytor kodu Lambda z wklejonym handlerem
# Zapisz kod jako lambda_function.py, potem spakuj
zip function.zip lambda_function.py

# Utwórz funkcję (zamień ACCOUNT_ID)
aws lambda create-function \
    --function-name NotesApiHandler \
    --runtime python3.12 \
    --handler lambda_function.lambda_handler \
    --role arn:aws:iam::ACCOUNT_ID:role/NotesApiLambdaRole \
    --zip-file fileb://function.zip \
    --timeout 10 \
    --region eu-central-1

Krok 4: Tworzymy API Gateway

Teraz wystawimy naszą Lambdę na świat przez API Gateway. Stworzymy REST API z odpowiednimi ścieżkami.

  1. Przejdź do API GatewayCreate API
  2. Wybierz REST API (nie REST API Private) → Build
  3. API name: NotesApi, Endpoint Type: Regional
  4. Kliknij Create API
  5. Utwórz resource /notes: kliknij Create resource, Resource name: notes
  6. Na /notes dodaj metody GET i POST: kliknij Create method
  7. Dla każdej metody: Integration type: Lambda Function, Lambda Function: NotesApiHandler, zaznacz Use Lambda Proxy integration
  8. Utwórz child resource {noteId} pod /notes: Resource Path: {noteId}
  9. Na /notes/{noteId} dodaj metody GET, PUT, DELETE - tak samo z Lambda Proxy
  10. Kliknij Deploy API, Stage: New stage, Stage name: prod
Screenshot: Struktura zasobów API Gateway z metodami HTTP
# Utwórz REST API
API_ID=$(aws apigateway create-rest-api \
    --name NotesApi \
    --endpoint-configuration types=REGIONAL \
    --query "id" --output text)

# Pobierz root resource ID
ROOT_ID=$(aws apigateway get-resources \
    --rest-api-id $API_ID \
    --query "items[?path=='/'].id" --output text)

# Utwórz /notes
NOTES_ID=$(aws apigateway create-resource \
    --rest-api-id $API_ID \
    --parent-id $ROOT_ID \
    --path-part notes \
    --query "id" --output text)

# Utwórz /notes/{noteId}
NOTE_ID=$(aws apigateway create-resource \
    --rest-api-id $API_ID \
    --parent-id $NOTES_ID \
    --path-part "{noteId}" \
    --query "id" --output text)

# Dla kazdej metody (przyklad GET /notes):
aws apigateway put-method \
    --rest-api-id $API_ID \
    --resource-id $NOTES_ID \
    --http-method GET \
    --authorization-type NONE

aws apigateway put-integration \
    --rest-api-id $API_ID \
    --resource-id $NOTES_ID \
    --http-method GET \
    --type AWS_PROXY \
    --integration-http-method POST \
    --uri "arn:aws:apigateway:eu-central-1:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-central-1:ACCOUNT_ID:function:NotesApiHandler/invocations"

# Powtórz put-method i put-integration dla:
# POST /notes, GET /notes/{noteId},
# PUT /notes/{noteId}, DELETE /notes/{noteId}

# Dodaj permission dla API Gateway
aws lambda add-permission \
    --function-name NotesApiHandler \
    --statement-id apigateway-access \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:eu-central-1:ACCOUNT_ID:$API_ID/*"

# Deploy
aws apigateway create-deployment \
    --rest-api-id $API_ID \
    --stage-name prod

echo "API URL: https://$API_ID.execute-api.eu-central-1.amazonaws.com/prod"
Nie zapomnij o Lambda permission! Jeśli tworzysz przez CLI, musisz ręcznie nadać API Gateway uprawnienie do wywoływania Lambdy (komenda add-permission powyżej). W konsoli AWS robi się to automatycznie.

Krok 5: Testujemy API

Twoje API powinno być już dostępne pod adresem wyświetlonym po deployu. Skopiuj URL i przetestuj wszystkie operacje:

# Zamień YOUR_API_URL na rzeczywisty URL z API Gateway
API="https://xxxxxxxxxx.execute-api.eu-central-1.amazonaws.com/prod"

# Utwórz notatkę
curl -X POST "$API/notes" \
  -H "Content-Type: application/json" \
  -d '{"title": "Moja pierwsza notatka", "content": "Serverless dziala!"}'

# Lista wszystkich notatek
curl "$API/notes"

# Pobierz konkretną notatkę (zamień NOTE_ID)
curl "$API/notes/NOTE_ID"

# Zaktualizuj notatkę
curl -X PUT "$API/notes/NOTE_ID" \
  -H "Content-Type: application/json" \
  -d '{"title": "Zaktualizowana notatka", "content": "Nowa tresc"}'

# Usun notatkę
curl -X DELETE "$API/notes/NOTE_ID"
Wskazówka: Jeśli dostajesz błąd 500, sprawdź logi w CloudWatch: Lambda → Monitor → View CloudWatch logs. Najczęstszą przyczyną jest brak uprawnień IAM do DynamoDB.

Cold starty i wydajność

Jedyne "ale" serverless to cold starty. Kiedy Lambda nie była wywoływana przez kilka minut, AWS musi uruchomić nowy kontener - to dodaje 200-500ms do pierwszego requesta. Z mojego doświadczenia przy codziennej pracy z Lambdą na projektach klientów ClearScale, cold starty w Pythonie są jednymi z najkrótszych.

Jak sobie z nimi radzić:

  • Provisioned Concurrency - utrzymuje "ciepłe" instancje (koszt ~$15/miesiąc za jedną), stosuj tylko jeśli potrzebujesz gwarantowanego czasu odpowiedzi poniżej 100ms
  • Małe paczki - im mniej zależności, tym szybszy start. Nasz handler importuje tylko boto3 (jest preinstalowany), więc start jest błyskawiczny
  • Trzymaj połączenie do DynamoDB poza handlerem - właśnie to zrobiliśmy deklarując dynamodb i table na górze pliku

Więcej o różnicach między serverless a tradycyjnym podejściem znajdziesz w artykule EC2 vs Lambda.

Ile to kosztuje?

To jest najlepsza część. Policzmy koszty dla typowego side-projectu lub małego MVP:

UsługaFree Tier (miesięcznie)Koszt po przekroczeniu
Lambda1 mln requestów + 400 000 GB-s$0.20 / 1 mln requestów
API Gateway1 mln wywołań (przez 12 miesięcy)$3.50 / 1 mln wywołań
DynamoDB (On-demand)25 GB storage, 25 WCU, 25 RCU$1.25 / 1 mln zapisów

Realistyczny scenariusz: Twoje API obsługuje 10 000 requestów dziennie (300 000/miesiąc). Koszt? Dokładnie $0 - mieścisz się w Free Tier. Nawet przy 100 000 requestów dziennie (3 mln/miesiąc) zapłacisz około $4-7 miesięcznie. Spróbuj osiągnąć coś podobnego na EC2 - sam koszt instancji t3.micro to już ~$8/miesiąc, niezależnie od ruchu.

Monitoring i debugowanie

Po wdrożeniu API warto skonfigurować podstawowy monitoring w CloudWatch:

  • Lambda Metrics - Invocations, Duration, Errors, Throttles - wszystko dostępne automatycznie
  • API Gateway Metrics - 4XX Errors, 5XX Errors, Latency, Count
  • Alarm - ustaw alarm na 5XX Errors > 0 w ciągu 5 minut, żeby dostać powiadomienie gdy coś się zepsuje

W Lambda console, zakładka Monitor pokazuje wykresy w czasie rzeczywistym. Kliknij "View CloudWatch logs" żeby zobaczyć logi z print() w Twoim handlerze. To bezcenne przy debugowaniu.

Zabezpieczanie API

Nasze API jest teraz publiczne - każdy znający URL może z niego korzystać. W produkcji warto dodać uwierzytelnianie:

  • API Key - najprostsze rozwiązanie, wystarczy dla wewnętrznych API i prototypów
  • Lambda Authorizer - własna logika autoryzacji (np. weryfikacja JWT tokena)
  • Cognito User Pools - pełne zarządzanie użytkownikami z rejestracją, logowaniem, MFA
  • IAM Authorization - dla komunikacji między serwisami AWS

Dodatkowo warto włączyć throttling w API Gateway (domyślnie 10 000 requestów/sekundę na konto). Dla naszego API ustaw rozsądny limit, np. 100 requestów/sekundę, żeby uchronić się przed przypadkowym lub celowym przeciążeniem.

Najczęściej zadawane pytania (FAQ)

Czy mogę użyć Node.js zamiast Pythona w Lambdzie?

Tak. Lambda obsługuje Node.js, Python, Java, Go, .NET i Ruby. Python i Node.js mają najkrótsze cold starty. Logika CRUD będzie wyglądać bardzo podobnie - zmieni się tylko składnia i SDK (AWS SDK for JavaScript v3, np. @aws-sdk/client-dynamodb i @aws-sdk/lib-dynamodb, zamiast boto3).

Jak wygląda skalowanie? Czy muszę coś konfigurować?

Nie musisz nic konfigurować. Lambda automatycznie skaluje się od 0 do tysięcy równoległych instancji. Domyślny limit to 1000 concurrent executions na region - możesz go zwiększyć przez AWS Support. API Gateway domyślnie obsługuje 10 000 requestów/sekundę.

Co jeśli moja Lambda przekroczy limit czasu (timeout)?

API Gateway zwróci błąd 504 Gateway Timeout. Domyślny timeout Lambdy to 3 sekundy, my ustawiliśmy 10. Maksymalny limit to 15 minut, ale API Gateway ma limit integracji - domyślnie 29 sekund (dla regionalnych REST API można go zwiększyć na wniosek, kosztem niższego limitu throttlingu). Jeśli operacja trwa dłużej, rozważ wzorzec asynchroniczny z SQS.

Czy mogę podłączyć własną domenę do API Gateway?

Tak. W API Gateway przejdź do Custom domain names, dodaj swoją domenę (np. api.twojadomena.pl), wgraj certyfikat SSL z ACM i skonfiguruj rekord CNAME w DNS. Cały proces zajmuje około 15 minut.

Serverless vs EC2 - kiedy wybrać które?

Serverless (Lambda) sprawdza się przy nieregularnym ruchu, MVP, mikroserwisach i prostych API. EC2 lepiej sprawdza się przy stałym, wysokim ruchu (powyżej milionów requestów dziennie), długo trwających procesach i aplikacjach wymagających pełnej kontroli nad systemem. Szczegółowe porównanie znajdziesz w artykule EC2 vs Lambda.

Następny krok

Masz działające REST API, ale to dopiero początek. Oto co warto zrobić dalej:

  • Dodaj uwierzytelnianie - API Key lub Cognito, w zależności od potrzeb
  • Wdróż infrastrukturę jako kod - przepisz wszystko do AWS SAM lub CloudFormation, żeby móc odtworzyć środowisko jedną komendą
  • Dodaj walidację - sprawdzaj przychodzące dane w Lambdzie (np. czy title nie jest pusty)
  • Skonfiguruj CI/CD - automatyczny deploy po pushu do repozytorium
  • Rozszerz monitoring - dodaj X-Ray tracing żeby śledzić czas każdej operacji DynamoDB

Jeśli dopiero zaczynasz przygodę z AWS, sprawdź kompletny przewodnik dla początkujących. A jeśli zastanawiasz się nad bezpieczeństwem swojego konta, koniecznie przeczytaj poradnik IAM.

Jeśli chcesz rozbudować swoje serverless API o powiadomienia i kolejki, sprawdź System powiadomień z SNS + SQS + Lambda.

Masz pytania? Napisz do mnie - chętnie pomogę z Twoim pierwszym serverless projektem.

Emil Kowalczyk

Pasjonat chmury i twórca CloudManiak.pl. Na co dzień MSP Engineer w amerykańskiej firmie ClearScale (AWS Premier Tier Partner). Pomagam osobom wchodzącym do świata chmury zdobywać wiedzę i certyfikaty.