Budowanie API to miejsce, gdzie backend-development spotyka się z prawdziwym światem. Niezależnie od tego, czy tworzysz prosty endpoint REST, czy złożony schemat GraphQL, Claude Code przekształca tworzenie API z żmudnego boilerplate’u w eleganckie, dobrze przetestowane usługi. Ta lekcja pokazuje, jak wykorzystać wsparcie AI do projektowania, implementacji i utrzymania API.
Scenariusz: Twój startup potrzebuje API obsługującego aplikacje mobilne, klientów internetowych i integracje z usługami trzecimi. Wymagania obejmują aktualizacje w czasie rzeczywistym, złożoną autoryzację, rate limiting i wsparcie dla 100K dziennie aktywnych użytkowników. Tradycyjne podejście: miesiące rozwoju. Z Claude Code: tygodnie.
Tydzień 1-2: Projektowanie API
- Ręczne pisanie specyfikacji OpenAPI
- Projektowanie przepływu autentykacji
Tydzień 3-4: Implementacja
- Pisanie boilerplate dla każdego endpointu
- Ręczna implementacja walidacji
- Dodanie obsługi błędów wszędzie
Tydzień 5-6: Testowanie i dokumentacja
- Pisanie testów dla każdego endpointu
- Tworzenie kolekcji Postman
- Dokumentowanie każdego parametru
Tydzień 7+: Udoskonalenia
- Naprawianie niespójności
- Dodawanie brakujących funkcji
- Optymalizacja wydajności
Dzień 1-2: Projektowanie i implementacja
> Zaprojektuj RESTful API dla naszej platformy e-commerce
> Uwzględnij autentykację, produkty, zamówienia i użytkowników
> Przestrzegaj najlepszych praktyk nazewnictwa i struktury
> Zaimplementuj API używając FastAPI z:
Dzień 3: Testowanie i dokumentacja
> Wygeneruj kompleksowe testy dla wszystkich endpointów
> Utwórz dokumentację OpenAPI
> Dodaj przykłady żądań i odpowiedzi
Dzień 4-5: Dopracowanie i wdrożenie
> Dodaj strategię cache'owania
> Zaimplementuj paginację
> Zaprojektuj RESTful API dla systemu zarządzania zadaniami z:
> - Autentykacją i autoryzacją użytkowników
> - Projektami, zadaniami i komentarzami
> - Powiadomieniami w czasie rzeczywistym
> Przestrzegaj najlepszych praktyk i konwencji REST
Claude generuje kompleksowy projekt API:
Struktura zasobów
GET /users # Lista użytkowników (tylko admin)
GET /users/{id} # Profil użytkownika
PUT /users/{id} # Aktualizacja użytkownika
DELETE /users/{id} # Usunięcie konta
GET /projects # Lista projektów użytkownika
POST /projects # Tworzenie projektu
GET /projects/{id} # Szczegóły projektu
PUT /projects/{id} # Aktualizacja projektu
DELETE /projects/{id} # Usunięcie projektu
GET /projects/{pid}/tasks # Lista zadań
POST /projects/{pid}/tasks # Tworzenie zadania
GET /tasks/{id} # Pobieranie zadania
PUT /tasks/{id} # Aktualizacja zadania
DELETE /tasks/{id} # Usunięcie zadania
PATCH /tasks/{id}/status # Aktualizacja tylko statusu
GET /tasks/{tid}/comments # Lista komentarzy
POST /tasks/{tid}/comments # Dodanie komentarza
PUT /comments/{id} # Edycja komentarza
DELETE /comments/{id} # Usunięcie komentarza
Konwencje HTTP
204 No Content - Udany DELETE
400 Bad Request - Nieprawidłowe dane
401 Unauthorized - Brak/nieprawidłowa auth
403 Forbidden - Brak uprawnień
404 Not Found - Zasób nie istnieje
409 Conflict - Duplikat/konflikt
429 Too Many Requests - Ograniczenie częstotliwości
Authorization: Bearer <token>
Content-Type: application/json
X-RateLimit-Remaining: 100
X-RateLimit-Reset: 1642345678
?page=1&limit=20 # Paginacja
?sort=created_at&order=desc # Sortowanie
?filter[status]=active # Filtrowanie
?include=user,project # Relacje
?fields=id,title,status # Wybrane pola
> Zaimplementuj API zarządzania zadaniami używając FastAPI:
> - Użyj SQLAlchemy dla bazy danych
> - Pydantic do walidacji
> - Uwzględnij middleware dla CORS i rate limiting
Claude generuje kompletną implementację:
from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.security import get_current_user
from app.api import auth, users, projects, tasks
from app.db.base import engine, Base
from app.middleware.rate_limit import RateLimitMiddleware
# Tworzenie tabel bazy danych
Base.metadata. create_all ( bind = engine )
# Inicjalizacja aplikacji FastAPI
title = " API zarządzania zadaniami " ,
description = " RESTful API do zarządzania zadaniami " ,
allow_origins = settings.ALLOWED_ORIGINS ,
period = 3600 # 100 wywołań na godzinę
app. include_router ( auth.router , prefix = " /api/auth " , tags = [ " auth " ] )
app. include_router ( users.router , prefix = " /api/users " , tags = [ " users " ] )
app. include_router ( projects.router , prefix = " /api/projects " , tags = [ " projects " ] )
app. include_router ( tasks.router , prefix = " /api/tasks " , tags = [ " tasks " ] )
async def health_check () :
return { " status " : " healthy " }
if __name__ == " __main__ " :
uvicorn. run ( app , host = " 0.0.0.0 " , port = 8000 )
from datetime import timedelta
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from sqlalchemy.orm import Session
from app.core.security import create_access_token, verify_password, get_password_hash
from app.core.config import settings
from app.db.session import get_db
from app.models.user import User
from app.schemas.auth import Token, UserCreate, UserLogin
@router.post ( " /register " , response_model = Token )
db : Session = Depends ( get_db )
# Sprawdzenie czy użytkownik istnieje
if db. query ( User ). filter ( User.email == user_data.email ). first ():
status_code = status.HTTP_409_CONFLICT ,
detail = " Email już jest zarejestrowany "
# Tworzenie nowego użytkownika
username = user_data.username ,
hashed_password = get_password_hash ( user_data.password )
access_token = create_access_token (
expires_delta = timedelta ( minutes = settings.ACCESS_TOKEN_EXPIRE_MINUTES )
" access_token " : access_token,
@router.post ( " /login " , response_model = Token )
form_data : OAuth2PasswordRequestForm = Depends (),
db : Session = Depends ( get_db )
# Autentykacja użytkownika
user = db. query ( User ). filter ( User.email == form_data.username ). first ()
if not user or not verify_password ( form_data.password , user.hashed_password ):
status_code = status.HTTP_401_UNAUTHORIZED ,
detail = " Nieprawidłowy email lub hasło "
access_token = create_access_token ( subject = str ( user.id ))
" access_token " : access_token,
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.db.session import get_db
from app.models.task import Task
from app.models.user import User
from app.schemas.task import TaskCreate, TaskUpdate, TaskResponse
from app.core.security import get_current_user
from app.core.pagination import paginate
@router.get ( " / " , response_model = List [ TaskResponse ])
page : int = Query ( 1 , ge = 1 ),
limit : int = Query ( 20 , ge = 1 , le = 100 ),
status : Optional[ str ] = None ,
project_id : Optional[ int ] = None ,
current_user : User = Depends ( get_current_user ),
db : Session = Depends ( get_db )
query = db. query ( Task ). filter ( Task.user_id == current_user.id )
query = query. filter ( Task.status == status )
query = query. filter ( Task.project_id == project_id )
return paginate ( query , page , limit )
@router.post ( " / " , response_model = TaskResponse , status_code = 201 )
current_user : User = Depends ( get_current_user ),
db : Session = Depends ( get_db )
# Weryfikacja dostępu do projektu
project = db. query ( Project ). filter (
Project.id == task_data.project_id ,
Project.user_id == current_user.id
detail = " Projekt nie został znaleziony "
@router.put ( " / {task_id} " , response_model = TaskResponse )
current_user : User = Depends ( get_current_user ),
db : Session = Depends ( get_db )
task = db. query ( Task ). filter (
Task.user_id == current_user.id
raise HTTPException ( status_code = 404 , detail = " Zadanie nie zostało znalezione " )
for field, value in task_update. dict ( exclude_unset = True ). items ():
setattr ( task , field , value )
> Zaimplementuj zaawansowaną paginację i filtrowanie dla endpointu zadań:
> - Paginację opartą na kursorze dla danych w czasie rzeczywistym
> - Złożone filtrowanie z operatorami
> - Wyszukiwanie pełnotekstowe
> - Sortowanie według wielu pól
# Zaawansowana obsługa zapytań
from typing import Optional, List
from fastapi import Query
from sqlalchemy import or_, and_
cursor : Optional[ str ] = None ,
limit : int = Query ( 20 , le = 100 ),
status : Optional[List[ str ]] = Query ( None ),
priority : Optional[ str ] = Query ( None ),
assigned_to : Optional[ int ] = None ,
created_after : Optional[datetime] = None ,
created_before : Optional[datetime] = None ,
search : Optional[ str ] = None ,
sort_by : List[ str ] = Query ( [ " created_at " ] ),
sort_order : List[ str ] = Query ( [ " desc " ] )
self .filters = self . _build_filters ( locals ())
self .sorting = list ( zip ( sort_by , sort_order ))
def apply_to_query ( self , query ) :
query = query. filter ( and_ ( * self .filters ))
# Zastosowanie wyszukiwania
Task.title. ilike ( f "% { self .search} %" ) ,
Task.description. ilike ( f "% { self .search} %" )
# Zastosowanie sortowania
for field, order in self .sorting:
column = getattr ( Task , field )
column. desc () if order == " desc " else column
# Zastosowanie paginacji kursorowej
query = query. filter ( Task.id > self . decode_cursor ( self .cursor ))
return query. limit ( self .limit + 1 )
from fastapi import Request, HTTPException
from starlette.middleware.base import BaseHTTPMiddleware
from collections import defaultdict
class RateLimitMiddleware ( BaseHTTPMiddleware ):
def __init__ ( self , app , calls : int = 100 , period : int = 3600 ) :
self .clients = defaultdict ( list )
self .cleanup_task = asyncio. create_task ( self . cleanup ())
async def dispatch ( self , request : Request, call_next ) :
# Pobranie identyfikatora klienta
client_id = request.client.host
if auth := request.headers. get ( " Authorization " ):
# Użycie ID użytkownika dla uwierzytelnionych żądań
client_id = self . get_user_id_from_token ( auth )
# Sprawdzenie limitu częstotliwości
requests = self .clients[client_id]
requests = [ req for req in requests if req > now - self .period ]
if len ( requests ) >= self .calls:
detail = " Przekroczono limit częstotliwości " ,
" X-RateLimit-Limit " : str ( self .calls ) ,
" X-RateLimit-Remaining " : " 0 " ,
" X-RateLimit-Reset " : str ( int ( requests [ 0 ] + self .period ))
self .clients[client_id] = requests
# Dodanie nagłówków limitu częstotliwości
response = await call_next ( request )
response.headers[ " X-RateLimit-Limit " ] = str ( self .calls )
response.headers[ " X-RateLimit-Remaining " ] = str ( self .calls - len ( requests ))
> Zaprojektuj schemat GraphQL dla tego samego systemu zarządzania zadaniami.
> - Definicje typów dla wszystkich encji
> - Zapytania do odczytu danych
> - Mutacje do modyfikacji
> - Subskrypcje dla aktualizacji w czasie rzeczywistym
> - Niestandardowe skalary dla dat
Claude generuje kompleksowy schemat GraphQL:
attachments : [ Attachment ! ] !
priority : TaskPriority = MEDIUM
hasPreviousPage : Boolean !
project ( id : ID ! ): Project
register ( input : RegisterInput ! ): AuthPayload !
login ( email : String ! , password : String ! ): AuthPayload !
refreshToken ( token : String ! ): AuthPayload !
createProject ( input : CreateProjectInput ! ): Project !
updateProject ( id : ID ! , input : UpdateProjectInput ! ): Project !
deleteProject ( id : ID ! ): Boolean !
createTask ( input : CreateTaskInput ! ): Task !
updateTask ( id : ID ! , input : UpdateTaskInput ! ): Task !
deleteTask ( id : ID ! ): Boolean !
batchUpdateTasks ( ids : [ ID ! ] ! , input : UpdateTaskInput ! ): [ Task ! ] !
uploadAttachment ( taskId : ID ! , file : Upload ! ): Attachment !
taskCreated ( projectId : ID ! ): Task !
taskUpdated ( id : ID ! ): Task !
taskDeleted ( id : ID ! ): ID !
projectUpdated ( id : ID ! ): Project !
userPresence ( projectId : ID ! ): PresenceEvent !
> Zaimplementuj GraphQL API używając frameworka Strawberry:
> - Użyj SQLAlchemy dla bazy danych
> - Dodaj DataLoader dla zapobiegania N+1
> - Zaimplementuj autentykację
> - Dodaj wsparcie dla subskrypcji
from typing import List, Optional
from datetime import datetime
from strawberry.types import Info
from app.graphql.types import User, Project, Task
from app.graphql.resolvers import (
from app.graphql.mutations import AuthMutations, TaskMutations
async def me ( self , info : Info ) -> User:
return await get_current_user ( info )
after : Optional[ str ] = None
user = await get_current_user ( info )
return await resolve_projects ( user , first , after )
after : Optional[ str ] = None ,
status : Optional[TaskStatus] = None ,
project_id : Optional[strawberry. ID ] = None
user = await get_current_user ( info )
return await resolve_tasks (
user , first , after , status , project_id
class Mutation ( AuthMutations , TaskMutations ):
self , info : Info, project_id : strawberry. ID
async for task in task_created_generator ( project_id ):
schema = strawberry. Schema (
subscription = Subscription
from strawberry.dataloader import DataLoader
from sqlalchemy.orm import Session
class UserLoader ( DataLoader ):
def __init__ ( self , db : Session ) :
super (). __init__ ( load_fn = self .load_users )
async def load_users ( self , user_ids : List[ int ] ) -> List[User]:
users = self .db. query ( User ). filter (
# Mapowanie w celu zachowania kolejności
user_map = {user.id: user for user in users}
return [ user_map. get ( uid ) for uid in user_ids ]
class ProjectLoader ( DataLoader ):
def __init__ ( self , db : Session ) :
super (). __init__ ( load_fn = self .load_projects )
async def load_projects ( self , project_ids : List[ int ] ) -> List[Project]:
projects = self .db. query ( Project ). filter (
Project.id. in_ ( project_ids )
project_map = {p.id: p for p in projects}
return [ project_map. get ( pid ) for pid in project_ids ]
async def get_context ( request ) :
" user_loader " : UserLoader ( get_db ()),
" project_loader " : ProjectLoader ( get_db ()),
" current_user " : await get_current_user_from_request ( request )
from typing import AsyncGenerator
from broadcaster import Broadcast
# Inicjalizacja broadcastera
broadcast = Broadcast ( " redis://localhost:6379 " )
class TaskCreatedSubscription :
project_id : strawberry. ID
) -> AsyncGenerator[Task, None ]:
async with broadcast. subscribe ( f "project: {project_id} :tasks" ) as subscriber:
async for event in subscriber:
if event[ " type " ] == " task_created " :
yield Task. from_dict ( event [ " data " ])
async def publish_task_created ( task : Task ) :
f "project: {task.project_id} :tasks" ,
> Wygeneruj kompleksowe testy dla naszego API:
> - Testy jednostkowe dla każdego endpointu
> - Testy integracyjne z bazą danych
> - Testy autentykacji/autoryzacji
from fastapi.testclient import TestClient
from sqlalchemy.orm import Session
from app.tests.utils import (
def test_create_task ( self , db : Session ) :
user = create_test_user ( db )
project = create_test_project ( db , user )
headers = get_auth_headers ( user )
" title " : " Zadanie testowe " ,
" description " : " Opis testowy " ,
" project_id " : project.id,
assert response.status_code == 201
assert data[ " title " ] == " Zadanie testowe "
assert data[ " priority " ] == " HIGH "
assert data[ " status " ] == " TODO "
def test_list_tasks_pagination ( self , db : Session ) :
user = create_test_user ( db )
project = create_test_project ( db , user )
create_test_task ( db , project , f "Zadanie {i} " )
headers = get_auth_headers ( user )
" /api/tasks/?limit=10&page=1 " ,
assert response.status_code == 200
assert len ( data [ " items " ]) == 10
assert data[ " total " ] == 25
" /api/tasks/?limit=10&page=2 " ,
assert len ( response. json () [ " items " ] ) == 10
def test_update_task_authorization ( self , db : Session ) :
user1 = create_test_user ( db , " user1@test.com " )
user2 = create_test_user ( db , " user2@test.com " )
project = create_test_project ( db , user1 )
task = create_test_task ( db , project )
# Próba aktualizacji zadania innego użytkownika
headers = get_auth_headers ( user2 )
json = { " title " : " Zhakowane! " }
assert response.status_code == 404 # Nie znaleziono dla nieautoryzowanego użytkownika
@pytest.mark.parametrize ( " invalid_data " , [
{ " title " : "" }, # Pusty tytuł
{ " title " : " A " * 256 }, # Za długi
{ " priority " : " INVALID " }, # Nieprawidłowy enum
{ " project_id " : 999999 } # Nieistniejący projekt
def test_create_task_validation ( self , db : Session, invalid_data ) :
user = create_test_user ( db )
headers = get_auth_headers ( user )
assert response.status_code == 422 # Błąd walidacji
from strawberry.test import GraphQLTestClient
from app.graphql.schema import schema
from app.tests.utils import create_test_user, create_test_project
def test_query_projects ( self , client : GraphQLTestClient ) :
query GetProjects($first: Int!) {
projects(first: $first) {
context_value = { " user " : create_test_user () }
assert " projects " in result.data
def test_create_task_mutation ( self , client : GraphQLTestClient ) :
mutation CreateTask($input: CreateTaskInput!) {
createTask(input: $input) {
user = create_test_user ()
project = create_test_project ( user )
" projectId " : str ( project.id ) ,
context_value = { " user " : user}
task = result.data[ " createTask " ]
assert task[ " title " ] == " Nowe zadanie "
assert task[ " status " ] == " TODO "
def test_subscription ( self , client : GraphQLTestClient ) :
subscription OnTaskCreated($projectId: ID!) {
taskCreated(projectId: $projectId) {
# Test konfiguracji subskrypcji
variables = { " projectId " : " 1 " }
# W prawdziwym teście, wywołałby tworzenie zadania
# i sprawdził czy subskrypcja otrzymuje zdarzenie
from locust import HttpUser, task, between
wait_time = between ( 1 , 3 )
# Logowanie w celu otrzymania tokenu
response = self .client. post ( " /api/auth/login " , json = {
" username " : " test@example.com " ,
self .token = response. json () [ " access_token " ]
self .headers = { " Authorization " : f "Bearer { self .token } " }
self .client. get ( " /api/tasks/ " , headers = self .headers )
self .client. get ( " /api/projects/1 " , headers = self .headers )
" title " : f "Zadanie {time. time () } " ,
# Uruchom z: locust -f test_performance.py --host=http://localhost:8000
> Zaimplementuj kompleksowe bezpieczeństwo dla naszego API:
> - JWT z tokenami odświeżania
> - Autentykację kluczem API dla usług
> - Kontrolę dostępu opartą na rolach
> - Rate limiting na użytkownika/IP
from datetime import datetime, timedelta
from typing import Optional, Union
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
pwd_context = CryptContext ( schemes = [ " bcrypt " ], deprecated = " auto " )
oauth2_scheme = OAuth2PasswordBearer ( tokenUrl = " /api/auth/login " )
def __init__ ( self , secret_key : str , algorithm : str = " HS256 " ) :
self .secret_key = secret_key
self .algorithm = algorithm
subject : Union[ str , int ],
expires_delta : Optional[timedelta] = None ,
additional_claims : dict = None
expire = datetime. utcnow () + (
expires_delta or timedelta ( minutes = 15 )
" iat " : datetime. utcnow (),
to_encode. update ( additional_claims )
return jwt. encode ( to_encode , self .secret_key , self .algorithm )
def create_refresh_token ( self , subject : Union[ str , int ] ) -> str :
expire = datetime. utcnow () + timedelta ( days = 7 )
return jwt. encode ( to_encode , self .secret_key , self .algorithm )
def decode_token ( self , token : str ) -> dict :
token , self .secret_key , algorithms = [ self .algorithm ]
except jwt.ExpiredSignatureError:
status_code = status.HTTP_401_UNAUTHORIZED ,
status_code = status.HTTP_401_UNAUTHORIZED ,
detail = " Nie można zweryfikować poświadczeń "
def __init__ ( self , resource : str , action : str ) :
def check ( self , user : User ) -> bool :
# Sprawdzenie czy użytkownik ma uprawnienie
return self in user. get_permissions ()
def __init__ ( self , permission : Permission ) :
self .permission = permission
def __call__ ( self , current_user : User = Depends ( get_current_user ) ) :
if not self .permission. check ( current_user ):
status_code = status.HTTP_403_FORBIDDEN ,
detail = " Niewystarczające uprawnienia "
dependencies = [ Depends ( RequirePermission ( Permission ( " task " , " delete " ))) ]
async def delete_task ( task_id : int ) :
from pydantic import BaseModel, validator, constr, conint
from typing import Optional
def __get_validators__ ( cls ) :
if not isinstance ( v , str ):
raise TypeError ( " Wymagany ciąg znaków " )
# Usunięcie niebezpiecznego HTML/skryptów
return bleach. clean ( v , tags = [], strip = True )
class TaskCreateSchema ( BaseModel ):
title: constr ( min_length = 1 , max_length = 200 )
description: Optional[SanitizedStr] = None
priority: conint ( ge = 1 , le = 4 )
def validate_title ( cls , v ) :
raise ValueError ( " Tytuł nie może być pusty " )
# Zapobieganie dodatkowym polom
> Wygeneruj kompleksową dokumentację API:
> - Specyfikację OpenAPI/Swagger
> - Przykłady żądań/odpowiedzi
> - Szczegóły autentykacji
> - Katalog odpowiedzi błędów
> - Strategię wersjonowania
# main.py - Rozszerzona dokumentacja
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
return app.openapi_schema
openapi_schema = get_openapi (
title = " API zarządzania zadaniami " ,
RESTful API do zarządzania zadaniami z aktualizacjami w czasie rzeczywistym.
Używa tokenów JWT Bearer. Uwzględnij w nagłówku Authorization:
Authorization: Bearer <token>
## Ograniczenie częstotliwości
- 100 żądań na godzinę dla uwierzytelnionych użytkowników
- 20 żądań na godzinę dla nieuwierzytelnionych
Standardowe kody statusu HTTP. Odpowiedzi błędów zawierają:
"field": "nazwa_pola" // Dla błędów walidacji
# Dodanie schematu bezpieczeństwa
openapi_schema[ " components " ] [ " securitySchemes " ] = {
openapi_schema[ " paths " ] [ " /api/tasks/ " ][ " post " ][ " requestBody " ][ " content " ][ " application/json " ][ " examples " ] = {
" summary " : " Proste zadanie " ,
" summary " : " Kompletne zadanie " ,
" title " : " Implementacja funkcji " ,
" description " : " Dodanie autentykacji użytkownika " ,
" due_date " : " 2024-12-31T23:59:59Z " ,
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
from fastapi import APIRouter
v1_router = APIRouter ( prefix = " /api/v1 " )
v2_router = APIRouter ( prefix = " /api/v2 " )
app. include_router ( v1_router )
app. include_router ( v2_router )
# Negocjacja wersji przez nagłówki
async def api_version_middleware ( request : Request, call_next ) :
version = request.headers. get ( " API-Version " , " v2 " )
request.state.api_version = version
response = await call_next ( request )
response.headers[ " API-Version " ] = version
Nauczyłeś się wykorzystywać Claude Code do szybkiego rozwoju API, od projektowania przez implementację po testowanie. Kluczem jest pozwolenie Claude’owi obsłużyć boilerplate, podczas gdy ty skupiasz się na logice biznesowej i decyzjach architektonicznych.
Pamiętaj: świetne API to coś więcej niż tylko endpointy - to spójność, bezpieczeństwo, wydajność i doświadczenie developera. Użyj Claude Code do egzekwowania najlepszych praktyk we wszystkich tych wymiarach, tworząc API, które są przyjemnością w budowaniu i używaniu.