Pular para conteúdo

API Design

Padrões e convenções para design de APIs REST e comunicação assíncrona.

REST APIs

Princípios

  • RESTful design
  • JSON para requests e responses
  • HTTP methods semânticos (GET, POST, PUT, PATCH, DELETE)
  • Status codes adequados
  • Versionamento quando necessário

Estrutura de URLs

# Coleções (plurais)
GET    /api/v1/users          # Listar usuários
POST   /api/v1/users          # Criar usuário
GET    /api/v1/users/{id}     # Obter usuário específico
PUT    /api/v1/users/{id}     # Atualizar usuário completo
PATCH  /api/v1/users/{id}     # Atualizar parcialmente
DELETE /api/v1/users/{id}     # Deletar usuário

# Sub-recursos
GET    /api/v1/users/{id}/orders       # Pedidos do usuário
POST   /api/v1/users/{id}/orders       # Criar pedido

# Ações (quando REST não se aplica bem)
POST   /api/v1/users/{id}/reset-password
POST   /api/v1/orders/{id}/cancel

HTTP Status Codes

2xx Success: - 200 OK - Request bem-sucedido (GET, PUT, PATCH) - 201 Created - Recurso criado (POST) - 204 No Content - Sucesso sem body (DELETE)

4xx Client Errors: - 400 Bad Request - Validação falhou - 401 Unauthorized - Não autenticado - 403 Forbidden - Autenticado mas sem permissão - 404 Not Found - Recurso não existe - 409 Conflict - Conflito (ex: email já existe) - 422 Unprocessable Entity - Validação semântica falhou - 429 Too Many Requests - Rate limit exceeded

5xx Server Errors: - 500 Internal Server Error - Erro não tratado - 502 Bad Gateway - Serviço downstream falhou - 503 Service Unavailable - Serviço temporariamente indisponível

Request/Response Format

Request (POST/PUT):

{
  "email": "user@example.com",
  "name": "John Doe",
  "age": 30
}

Response Success:

{
  "id": 123,
  "email": "user@example.com",
  "name": "John Doe",
  "age": 30,
  "created_at": "2026-01-20T10:00:00Z",
  "updated_at": "2026-01-20T10:00:00Z"
}

Response Error:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid input data",
    "details": [
      {
        "field": "email",
        "message": "Invalid email format"
      },
      {
        "field": "age",
        "message": "Must be between 0 and 150"
      }
    ]
  }
}

Pagination

GET /api/v1/users?page=2&page_size=20&sort_by=created_at&order=desc

Response:

{
  "data": [...],
  "pagination": {
    "page": 2,
    "page_size": 20,
    "total_items": 150,
    "total_pages": 8,
    "has_next": true,
    "has_prev": true
  }
}

Filtering

GET /api/v1/users?status=active&role=admin&created_after=2026-01-01

Autenticação

JWT Bearer Token:

GET /api/v1/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

FastAPI Implementation:

from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer

security = HTTPBearer()

async def get_current_user(token: str = Depends(security)) -> User:
    try:
        payload = jwt.decode(token.credentials, SECRET_KEY)
        user_id = payload.get("sub")
        user = await get_user(user_id)
        if not user:
            raise HTTPException(status_code=401)
        return user
    except JWTError:
        raise HTTPException(status_code=401)

@router.get("/me")
async def get_current_user_endpoint(
    current_user: User = Depends(get_current_user)
):
    return current_user

Event-Driven Architecture

SQS Pattern

Producer (Lambda):

import boto3
import json

sqs = boto3.client('sqs')

def publish_user_created_event(user: User):
    sqs.send_message(
        QueueUrl=USER_EVENTS_QUEUE_URL,
        MessageBody=json.dumps({
            "event_type": "user_created",
            "event_id": str(uuid.uuid4()),
            "timestamp": datetime.utcnow().isoformat(),
            "data": {
                "user_id": user.id,
                "email": user.email
            }
        }),
        MessageAttributes={
            'event_type': {
                'StringValue': 'user_created',
                'DataType': 'String'
            }
        }
    )

Consumer (Lambda):

def lambda_handler(event, context):
    for record in event['Records']:
        message = json.loads(record['body'])
        event_type = message['event_type']

        if event_type == 'user_created':
            handle_user_created(message['data'])
        elif event_type == 'user_updated':
            handle_user_updated(message['data'])
        else:
            logger.warning(f"Unknown event type: {event_type}")

SNS Pattern (Fan-out)

Publisher:

sns = boto3.client('sns')

def publish_product_update(product: Product):
    sns.publish(
        TopicArn=PRODUCT_UPDATES_TOPIC_ARN,
        Message=json.dumps({
            "product_id": product.id,
            "action": "price_updated",
            "old_price": product.old_price,
            "new_price": product.price
        }),
        Subject="Product Price Updated"
    )

Subscribers (múltiplos Lambdas):

# Lambda 1: Atualizar cache
def update_cache_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    cache.invalidate(f"product:{message['product_id']}")

# Lambda 2: Notificar usuários
def notify_users_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    users = get_interested_users(message['product_id'])
    send_notifications(users, message)

# Lambda 3: Atualizar analytics
def analytics_handler(event, context):
    message = json.loads(event['Records'][0]['Sns']['Message'])
    track_price_change(message)

Idempotência

APIs devem ser idempotentes quando possível:

@router.post("/orders", status_code=201)
async def create_order(
    request: CreateOrderRequest,
    idempotency_key: str = Header(...)
):
    # Verificar se já processamos este idempotency_key
    existing = await get_order_by_idempotency_key(idempotency_key)
    if existing:
        return existing  # Retornar ordem existente (idempotente)

    # Criar nova ordem
    order = await service.create_order(request, idempotency_key)
    return order

Rate Limiting

from fastapi_limiter.depends import RateLimiter

@router.post("/login", dependencies=[Depends(RateLimiter(times=5, seconds=60))])
async def login(credentials: LoginRequest):
    # Máximo 5 tentativas por minuto
    ...

Versionamento

/api/v1/users  # Versão 1 (atual)
/api/v2/users  # Versão 2 (nova, coexiste com v1)

Quando criar nova versão: - Breaking changes na API - Mudanças significativas no formato de resposta - Remoção de campos

Deprecação: - Avisar clientes com 3 meses de antecedência - Header X-API-Deprecation: version=v1, sunset=2026-06-01 - Manter v1 rodando até data de sunset

Referências