FastAPI + HTMX: o full-stack sem build
# Crie aplicativos web de produção sem React ou webpack: FastAPI, HTMX, Alpine.js, Jinja2, CSS puro, padrões Bootstrap, i18n, implantação, SEO e desempenho.
Resumo: FastAPI + HTMX + Alpine.js + Jinja2 + CSS puro produz aplicações web de produção com zero ferramentas de build, zero
node_modules/e pontuações perfeitas no Lighthouse. Este guia cobre o sistema inteiro, da arquitetura ao deploy, usando blakecrosley.com como referência de produção que serve 210 posts de blog, componentes interativos de JavaScript, 11 guias principais, 48 estudos de design e inglês mais 9 locales traduzidos sem um único bundler, compilador ou transpilador.1
A stack moderna de desenvolvimento web parte do princípio de que você precisa de React, webpack, TypeScript e um pipeline de build. Para uma grande categoria de aplicações — sites orientados por conteúdo, ferramentas internas, aplicações CRUD, sites de portfólio, plataformas de documentação — essa premissa está errada. A stack descrita neste guia elimina toda a cadeia de ferramentas de build do frontend enquanto produz sites que alcançam 100/100/100/100 no Lighthouse.2
Isto não é defesa de uma ideia. É uma medição. A arquitetura descrita aqui roda em produção, atende usuários reais em dez idiomas, e os números são verificáveis.
Principais conclusões
- HTML renderizado no servidor elimina três categorias inteiras de problemas: gerenciamento de estado no cliente, fronteiras de serialização JSON e incompatibilidades de hidratação. HTMX torna as respostas do servidor a saída final — sem etapa de renderização no cliente.
- Zero ferramentas de build significa zero falhas de build. Nenhum conflito de peer dependencies do
npm install, nenhum erro do compilador TypeScript em arquivos que você não tocou, nenhum PR do Dependabot para dependências transitivas que você nunca importou. O pipeline de deploy égit push. - Alpine.js lida com o estado puramente do cliente que HTMX não consegue. Dropdowns, modais, alternadores de navegação mobile e qualquer estado de UI que existe puramente no navegador pertencem ao Alpine.js. A fronteira é clara: se o estado precisa do servidor, use HTMX. Se não precisa, use Alpine.js.
- CSS puro com custom properties substitui Sass e Tailwind. As custom properties do CSS cascateiam, herdam e respondem a media queries em tempo de execução. Variáveis de pré-processador compilam para valores estáticos e desaparecem. O navegador lê as custom properties diretamente — sem etapa de compilação.
- Esta abordagem tem fronteiras claras. Ela é inadequada para grandes equipes que compartilham interfaces de componentes, produtos SaaS com estado complexo no cliente e aplicações que dependem de bibliotecas do ecossistema npm. O framework de decisão na Seção 15 identifica a fronteira com precisão.
- blakecrosley.com é a prova. Os padrões centrais deste guia (HTMX, Alpine.js, Jinja2, CSS puro) rodam em produção em blakecrosley.com. As seções de Bootstrap e SQLAlchemy cobrem padrões padrão para o stack que não são usados neste site específico. Toda afirmação tem um caminho de arquivo, um bloco de configuração ou uma auditoria do Lighthouse que você mesmo pode verificar em PageSpeed Insights.2
Como usar este guia
Esta é uma referência abrangente. Comece onde seu nível de experiência se encaixa:
| Experiência | Comece aqui | Depois explore |
|---|---|---|
| Desenvolvedor Python, novo em HTMX | A tese no-build → Visão geral da arquitetura → HTMX em profundidade | Padrões de Alpine.js, Segurança |
| Desenvolvedor React/Vue avaliando alternativas | A tese no-build → Framework de decisão | Visão geral da arquitetura, Performance |
| Desenvolvedor FastAPI adicionando interatividade | HTMX em profundidade → Padrões de Alpine.js | i18n e localização, Deploy |
| Desenvolvedor full-stack começando do zero | Leia sequencialmente a partir de Visão geral da arquitetura | Cartão de referência rápida para uso contínuo |
Use Ctrl+F / Cmd+F para buscar padrões ou atributos específicos. O Cartão de referência rápida ao final fornece um resumo escaneável.
A tese no-build
A tese é estreita e específica: para sites orientados a conteúdo com um desenvolvedor solo ou uma equipe pequena, ferramentas de build resolvem problemas que você não tem enquanto criam problemas que você tem.
Aqui estão as métricas reais de blakecrosley.com:
| Métrica | blakecrosley.com (No-Build) | Projeto Next.js típico3 |
|---|---|---|
| Dependências | 17 pacotes Python | 311+ pacotes npm |
| Arquivos de configuração de build | 0 | 5-8 (next.config, tsconfig, postcss, tailwind, etc.) |
Tamanho de node_modules/ |
Não existe | 187 MB de base, 250-400 MB com adições |
| Tempo de instalação | pip install: 8 segundos |
npm install: 30-90 segundos |
| Etapa de build | Nenhuma | next build: 15-60 segundos |
| Pipeline de deploy | git push → no ar em ~40 segundos |
Install → build → deploy: 2-5 minutos |
| Lighthouse Performance | 100 | 70-90 sem otimização explícita4 |
Os 17 pacotes Python incluem FastAPI, Jinja2, Pydantic, uvicorn, nh3 e outros 12. Nenhum é uma ferramenta de build. Nenhum é um compilador. Nenhum é um bundler.5
O que você abre mão
Honestidade exige listar os custos reais:
Sem TypeScript. Todo arquivo .js é JavaScript puro. Erros de tipo são detectados por testes e análise de código, não por um compilador. Isso funciona para um desenvolvedor solo. Não funcionaria para uma equipe de 10 compartilhando interfaces de componentes.
Sem Hot Module Replacement. Mudanças de CSS exigem um refresh manual do navegador. O hx-boost do HTMX torna a navegação rápida o suficiente para que refreshes completos sejam toleráveis, mas em ciclos de iteração visual apertados, o HMR economiza tempo.
Sem Tree Shaking. Cada byte de JavaScript que você escreve vai para o navegador. A restrição força disciplina: arquivos pequenos e focados em vez de grandes módulos utilitários.
Sem bibliotecas de componentes do npm. Sem Radix, sem shadcn/ui, sem Headless UI. Todo elemento interativo é construído à mão ou usa os componentes embutidos do Bootstrap 5.
Sem tokens de design system do npm. O design system vive em custom properties do CSS. Ele não pode ser importado como um pacote em outro projeto.
Esses tradeoffs são aceitáveis para um site orientado a conteúdo com um a três desenvolvedores. Eles seriam inaceitáveis para um produto SaaS com uma equipe de engenharia de 15 pessoas. A Seção 15 fornece o framework de decisão.
O que você ganha
Zero falhas de build. Nenhum npm install pode falhar devido a conflitos de peer dependencies. Nenhum next build pode falhar devido a um erro de TypeScript em um arquivo que você não tocou.6
Debug com View Source. O JavaScript rodando no navegador é o JavaScript que você escreveu. Sem source maps necessários.
Inicialização local instantânea. uvicorn app.main:app --reload inicia em menos de 2 segundos.
Cascata de requisições concreta. Uma primeira visita carrega: um documento HTML (~15KB gzipped), um arquivo CSS (~8KB), HTMX (~16KB, em cache), Alpine.js (~15KB, em cache) e o JS interativo da página (~4-8KB). Total: aproximadamente 55-65KB na primeira visita.1
Frontend à prova de futuro. O código do lado do cliente usa HTML, CSS e JavaScript — padrões que mantiveram compatibilidade retroativa por 30 anos.7 Sem migração de Webpack 4 → 5, sem deprecação do Create React App, sem migração do App Router do Next.js.
Comparação de stacks
Como o stack no-build se compara a alternativas comuns em dimensões mensuráveis:
| Dimensão | FastAPI+HTMX (este guia) | Next.js (React) | Astro | 11ty |
|---|---|---|---|---|
| JS enviado ao navegador | 35-40KB (HTMX+Alpine+pequenos scripts de página) | 85-250KB+ (runtime do React) | 0KB por padrão, islands opt-in | 0KB por padrão |
| Etapa de build | Nenhuma | Obrigatória (webpack/turbopack) | Obrigatória (Vite) | Obrigatória (customizada) |
| Arquivos de configuração | 0 | 5-8 (next.config, tsconfig, etc.) | 1-3 (astro.config, tsconfig) | 1-2 (.eleventy.js) |
| Pipeline de deploy | git push (40s) |
Install+build+deploy (2-5min) | Install+build+deploy (1-3min) | Install+build+deploy (1-2min) |
| Interatividade no servidor | Nativa (HTMX) | Rotas API + fetch no cliente | Limitada (form actions) | Nenhuma (saída estática) |
| Gerenciamento de estado no cliente | Alpine.js (15KB) | state/context/Redux do React | Islands do framework | JS manual |
| Linguagem de backend | Python | JavaScript/TypeScript | JavaScript/TypeScript | JavaScript |
| Abordagem de i18n | Lado do servidor (middleware) | next-intl ou pacote similar | @astrojs/i18n | Manual |
| Lighthouse Performance | 100 (medido) | 70-90 típico4 | 95-100 típico | 95-100 típico |
| Melhor para | Sites de conteúdo, CRUD, dashboards | SPA complexa, equipes grandes | Sites de conteúdo, marketing | Blogs estáticos, docs |
Astro e 11ty são os concorrentes mais próximos para sites de conteúdo. Ambos produzem excelente saída estática, mas exigem uma etapa de build e uma toolchain JavaScript. O stack FastAPI+HTMX troca a performance de site estático por interatividade no servidor (filtragem de categorias, manipulação de formulários, busca em tempo real) sem adicionar uma etapa de build. Se seu site é puramente estático sem interações com o servidor, Astro ou 11ty podem ser a melhor escolha.
Visão geral da arquitetura
Fluxo de requisições
Cada requisição segue um único caminho através de quatro camadas:
Browser FastAPI Jinja2 HTMX/Alpine
| | | |
|--- GET /about ------>| | |
| |-- render template ->| |
| | |-- base.html ------->|
| | | + about.html |
| |<-- full HTML -------| |
|<--- HTML response ---| | |
| |
|--- hx-get /search ------------------------------------------------>|
| |<-- HTMX request ----| |
| |-- render partial -->| |
| | |-- _results.html |
| |<-- HTML fragment ---| |
|<--- HTML fragment ---| | |
|--- DOM swap -------------------------------------------------------->|
Carregamentos de página completos retornam documentos HTML inteiros (template base + template da página). Requisições HTMX retornam fragmentos HTML (partials). O servidor decide o que renderizar com base no tipo de requisição. Alpine.js gerencia o estado exclusivo do cliente, que nunca chega ao servidor.
Papel de cada componente
| Componente | Papel | Escopo |
|---|---|---|
| FastAPI | Roteamento, lógica de negócios, acesso a dados, validação | Servidor |
| Jinja2 | Renderização de templates, herança, macros | Servidor |
| HTMX | Interatividade orientada pelo servidor (formulários, paginação, busca) | Cliente ↔ Servidor |
| Alpine.js | Estado exclusivo do cliente (dropdowns, modais, toggles) | Apenas cliente |
| Bootstrap 5 | Sistema de grid, classes utilitárias, layout responsivo | Cliente (CSS) |
| CSS puro | Propriedades customizadas, estilos de componentes, design tokens | Cliente (CSS) |
| Pydantic | Validação de request/response, configurações | Servidor |
Estrutura do projeto
app/
├── main.py # FastAPI app, middleware, templates
├── config.py # Pydantic settings management
├── routes/
│ ├── pages.py # Page routes (HTML responses)
│ └── api.py # API routes (JSON/HTML fragment responses)
├── content.py # Markdown loading, blog post parsing
├── security/
│ ├── headers.py # CSP, HSTS, security headers middleware
│ ├── csrf.py # HMAC-signed CSRF tokens
│ ├── rate_limit.py # 3-tier rate limiting
│ └── logging.py # Security event logging
├── i18n/
│ ├── config.py # Supported locales, mappings
│ ├── middleware.py # URL-based locale detection
│ ├── jinja.py # Translation functions for templates
│ └── d1_client.py # Cloudflare D1 translation storage
├── cache_assets.py # Content-hash asset versioning
└── templates/
├── base.html # Base layout with Alpine.js state
├── components/ # Reusable partials (_language_switcher.html, etc.)
└── pages/ # Page templates (home.html, about.html, etc.)
content/
├── blog/ # Markdown blog posts with YAML frontmatter
└── guides/ # Multi-section guide markdown
static/
├── css/ # Plain CSS (no preprocessors)
├── js/ # Vanilla JavaScript (no bundlers)
│ └── vendor/ # Self-hosted HTMX, Alpine.js
└── images/ # Optimized images with WebP srcset
A estrutura segue um único princípio: cada diretório contém um tipo de coisa. Rotas ficam em routes/. Templates ficam em templates/. Arquivos estáticos ficam em static/. Nenhuma etapa de build transforma um no outro.
Comparação com a arquitetura SPA
Em um projeto React + Next.js, a estrutura equivalente incluiria:
src/
├── components/ # React components (JSX)
├── pages/ # Route handlers (also JSX)
├── api/ # API routes (also in pages/)
├── hooks/ # Custom React hooks
├── context/ # React context providers
├── lib/ # Utility functions
├── styles/ # CSS modules or Tailwind config
└── types/ # TypeScript type definitions
# Plus build configuration
next.config.js
tsconfig.json
postcss.config.js
tailwind.config.js
eslint.config.js
package.json
package-lock.json
node_modules/ # 187+ MB of dependencies
A arquitetura SPA exige coordenação em tempo de build entre esses diretórios. TypeScript compila .tsx para JavaScript. PostCSS processa diretivas Tailwind em CSS. Webpack (ou Turbopack) empacota a saída em chunks. Cada etapa pode falhar de forma independente.
A arquitetura sem build não exige coordenação. O template referencia um arquivo CSS. O arquivo CSS existe em static/css/. O navegador o carrega diretamente. Se você renomear um arquivo, a referência no template quebra em tempo de execução — não em tempo de build. Isso transfere os erros do tempo de compilação para o tempo de execução, o que é uma troca real. Para um desenvolvedor solo rodando uvicorn --reload durante o desenvolvimento, erros em tempo de execução aparecem imediatamente no navegador. Para uma equipe grande, erros em tempo de compilação capturados pelo TypeScript previnem uma categoria de bugs que erros em tempo de execução não conseguem.
Padrões de FastAPI
Configuração da aplicação
A aplicação é inicializada em main.py com uma ordem explícita de middleware:
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from starlette.middleware.gzip import GZipMiddleware
app = FastAPI(
title="Blake Crosley",
docs_url=None, # Disable docs in production
redoc_url=None,
openapi_url=None, # Prevent /openapi.json exposure
)
# Middleware order matters: last added = first executed
app.add_middleware(SecurityHeadersMiddleware)
app.add_middleware(GZipMiddleware, minimum_size=500)
app.add_middleware(LocaleMiddleware)
app.add_middleware(RateLimitMiddleware)
app.add_middleware(SecurityLogMiddleware, site_name="blakecrosley.com")
# Static files
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
# Templates
templates = Jinja2Templates(directory=TEMPLATES_DIR)
Três decisões de design importam aqui. Primeiro, docs_url=None e openapi_url=None desativam os endpoints automáticos de documentação do API. Um site de conteúdo voltado ao público não precisa expor /docs nem /openapi.json na internet.8 Segundo, a ordem do middleware importa — o registro de segurança é executado primeiro (adicionado por último), portanto captura todas as solicitações, inclusive as rejeitadas pela limitação de taxa. Terceiro, GZipMiddleware compacta respostas com mais de 500 bytes, o que normalmente reduz o tamanho de transferência do HTML em 70-80%. Desde o Starlette 1.5.0, ele não compacta mais tudo: uma lista de exclusão padrão agora ignora payloads binários e já compactados (arquivos gzip e zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/*, fontes WOFF e WOFF2 e text/event-stream), que é o desejável — recompatar um PNG consome CPU para deixá-lo ligeiramente maior. Observe que a lista exclui deliberadamente image/*, então image/svg+xml ainda é compactado. Substitua-a com o parâmetro somente por palavra-chave exclude_content_types.28
Roteamento
As rotas se dividem em duas categorias: as rotas de página retornam documentos completos do HTML, e as rotas do API retornam fragmentos de JSON ou HTML.
# routes/pages.py — full HTML responses
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/about")
async def about(request: Request):
templates = request.app.state.templates
return templates.TemplateResponse("pages/about.html", {
"request": request,
"page_title": "About — Blake Crosley",
"page_description": "Designer, developer, dad.",
})
# routes/api.py — JSON or HTML fragment responses
@router.get("/api/quiz/{quiz_id}/step")
async def quiz_step(request: Request, quiz_id: str, answers: str = ""):
# Parse answers, compute next question or result
question = get_next_question(quiz_id, answers)
templates = request.app.state.templates
return templates.TemplateResponse("components/_quiz_step.html", {
"request": request,
"question": question,
"answers": answers,
"step": len(answers.split(",")) if answers else 0,
})
A distinção importa para o HTMX. As rotas de página completas retornam documentos que estendem base.html. As rotas do API retornam fragmentos de HTML que o HTMX troca em elementos existentes do DOM. O mesmo mecanismo de templates do Jinja2 renderiza ambos — sem uma camada separada de API.
Injeção de dependência
O sistema Depends() do FastAPI oferece uma separação clara entre os manipuladores de rota e a lógica compartilhada:
from fastapi import Depends, Request
def get_templates(request: Request):
"""Get templates from app state."""
return request.app.state.templates
def get_current_locale(request: Request) -> str:
"""Get locale from middleware-set request state."""
return getattr(request.state, "locale", "en")
@router.get("/blog/{slug}")
async def blog_post(
request: Request,
slug: str,
templates=Depends(get_templates),
locale: str = Depends(get_current_locale),
):
post = load_post_by_slug(slug)
if not post:
raise HTTPException(404, "Post not found")
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"locale": locale,
})
As dependências são compostas. Uma dependência get_db pode depender de get_current_locale, que depende da solicitação. O FastAPI resolve a cadeia automaticamente.
Configurações do Pydantic
A configuração usa BaseSettings do Pydantic com precedência de variáveis de ambiente:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
D1_WORKER_URL: str = ""
D1_AUTH_SECRET: str = ""
CLOUDFLARE_ACCOUNT_ID: str = ""
CLOUDFLARE_API_TOKEN: str = ""
ANALYTICS_PASSKEY: str = ""
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
As variáveis de ambiente substituem os valores do arquivo .env. Em produção (Railway), os segredos são definidos como variáveis de ambiente. Localmente, um arquivo .env fornece valores padrão. A classe Settings valida os tipos na inicialização — um campo obrigatório ausente falha rapidamente, em vez de falhar em tempo de execução.
Padrões async
As rotas do FastAPI são async por padrão. Para operações vinculadas a I/O (consultas ao banco de dados, solicitações HTTP, leituras de arquivos), async evita bloquear o loop de eventos:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory cache at startup
async with httpx.AsyncClient() as client:
for locale in SUPPORTED_LOCALES:
resp = await client.post(f"{D1_URL}/query", ...)
TRANSLATIONS[locale] = resp.json()["results"]
yield
# Cleanup on shutdown (if needed)
app = FastAPI(lifespan=lifespan)
Lifespan agora é o único caminho de inicialização/encerramento. O Starlette chegou à sua primeira versão estável, 1.0, em março de 2026 (1.6.0 em 8 de agosto) e removeu os hooks há muito obsoletos on_event, on_startup e on_shutdown — lifespan (acima) é o único mecanismo, e @app.route() / @app.websocket_route() deram lugar a Route / WebSocketRoute na lista routes. O FastAPI 0.137.0 (14 de junho de 2026) refatora os próprios componentes internos do roteador: router.routes deixou de ser uma lista plana de objetos APIRoute e passou a ser uma árvore de nós intermediários, portanto trate-o como um detalhe interno, não como algo a iterar. A vantagem é que rotas adicionadas a um roteador depois de include_router() agora são refletidas em tempo real, e um sub-roteador pode ser incluído antes de suas rotas serem definidas. O próprio FastAPI não fixa o Starlette na linha 1.x: seu requisito de runtime tem sido apenas um piso, starlette>=0.46.0, desde a 0.136.3 e permanece inalterado até a 0.140.7 — sem limite superior, e o Starlette 0.4x ainda o satisfaz. Os números de versão 1.x nas notas de lançamento da 0.137.0 são atualizações do dependabot no próprio lockfile de testes do repositório, não uma restrição de runtime para seu aplicativo.24 Nada disso altera os padrões deste guia — ele usa lifespan e declaração de rota padrão em toda parte —, mas, se você mantém ferramentas que percorrem router.routes ou ainda executa manipuladores legados @app.on_event, 0.137.0 / Starlette 1.0 são incompatíveis. O FastAPI 0.137.2 (18 de junho de 2026) dá sequência com iter_route_contexts(), a forma compatível de enumerar rotas agora que router.routes é interno. O FastAPI 0.138.0 (20 de junho de 2026) adiciona então app.frontend("/", directory="dist") / router.frontend(...) para servir um frontend estático compilado — útil se você entrega uma build de SPA separada, mas sem relação com a abordagem deste guia de não usar build e renderizar no servidor (ele monta um diretório dist/, em vez de renderizar o HTML no servidor).25 O FastAPI 0.139.0 (1º de julho de 2026) estende isso com suporte a dependências em app.frontend() — por exemplo, autenticação automática por cookie para o frontend servido —, levando a mesma mecânica de Depends() que você usa em rotas do API para a montagem do frontend estático.26 O FastAPI 0.141.0 (29 de julho de 2026) adiciona app.frontend(check_dir="auto"), que impede fastapi dev de falhar quando o diretório de build ainda não existe — o caso comum em que você inicia o servidor antes de executar a build do frontend. O FastAPI 0.141.1, lançado no mesmo dia, corrige dependências em app.frontend() que descartavam silenciosamente tarefas em segundo plano e cabeçalhos de resposta: uma dependência que definia um cookie ou agendava uma BackgroundTask tinha esse trabalho descartado na montagem do frontend, embora funcionasse normalmente em rotas do API. Se você adotou o suporte a dependências da 0.139.0, a 0.141.1 é a versão que faz com que ele se comporte como o restante do aplicativo.29
O FastAPI 0.140.0 encerra uma regressão de memória presente em todas as versões desde novembro de 2025 — atualize. A versão de 24 de julho de 2026 traz uma única refatoração com um efeito desproporcional. Dependant, o objeto interno que o FastAPI cria para cada nó do grafo de dependências de cada rota, acumulava atributos functools.cached_property desde a 0.121.0 (3 de novembro de 2025) — eram dez deles na 0.139.2. Uma propriedade em cache precisa de um __dict__ por instância para gravar seu resultado, então o custo se multiplicava em cada nó de cada grafo do aplicativo. A PR #16049 move essa lógica para helpers no nível do módulo (_get_cache_key(), _get_oauth_scopes(), _uses_scopes()) e declara Dependant como um @dataclass(slots=True), deixando-o como um contêiner de dados puro. A execução de CodSpeed do próprio FastAPI na PR mesclada coloca o benchmark de memória test_dependency_graph em 17,5 MB → 1,1 MB, uma redução de ×16; o relatório que motivou o trabalho descreveu um serviço de produção que permanecia abaixo de aproximadamente 400 MB na 0.120.4 e atingia OOM na 0.121.3. Todas as versões recomendadas por este guia desde então — 0.137.x, 0.138.0, 0.139.2 — carregavam esse problema. Se o seu aplicativo tem uma árvore de dependências profunda ou ampla (Depends() aninhado, esquemas de segurança, muitos roteadores incluídos), a 0.140.0 oferece um ganho de memória gratuito sem exigir alterações no código da aplicação.27
0.140.0 foi o primeiro passo, não a correção completa — fixe em 0.140.7 ou posterior. Três dias após essa versão, em 27 de julho de 2026, o FastAPI lançou mais sete versões em cinco horas e meia: 0.140.1 até 0.140.7, todas refatorando a mesma mecânica de dependências. O trabalho se divide em duas partes. Primeiro, a árvore plana de dependências: o FastAPI costumava criar e manter uma cópia achatada do grafo de dependências de cada rota, e a 0.140.2 deixa de retê-la; a 0.140.3, 0.140.5, 0.140.6 e 0.140.7 removem os locais restantes que ainda recriavam uma — geração de OpenAPI, campos de corpo, parâmetros de solicitação e OpenAPI novamente. A 0.140.4 remove controles que rastreavam dependências repetidas que nada lia. Segundo, e a única alteração com um limiar visível: a 0.140.1 eleva o lru_cache dos helpers de classificação de chamadas em fastapi/dependencies/models.py de 1.024 para 4.096 entradas, por trás de uma constante nomeada _CALLABLE_CLASSIFICATION_CACHE_SIZE, porque usuários relataram que aplicativos com mais de 1.024 dependências distintas faziam o cache oscilar. Nada aqui altera um API que você chama, portanto atualizar é apenas mudar a versão. Vale declarar claramente duas ressalvas: o ritmo indica que a linha ainda está em evolução, então leia as notas de lançamento em vez de assumir que a 0.140.7 é o fim; e o FastAPI adicionou os benchmarks de dependência do OpenAPI que medem esse trabalho na mesma janela (PR #16075), portanto os números publicados abrangem as últimas poucas versões, não todo o arco de sete versões.30
Operações vinculadas à CPU (renderização de Markdown, extração de CSS) podem usar funções síncronas. O FastAPI as executa automaticamente em um pool de threads quando o manipulador de rota não é declarado como async:
# Sync function — FastAPI runs it in a thread pool
@router.get("/blog/{slug}")
def blog_post(slug: str):
post = load_post_by_slug(slug) # CPU-bound Markdown parsing
return templates.TemplateResponse(...)
A regra: se a função aguarda I/O, torne-a async. Se ela realiza trabalho de CPU, mantenha-a síncrona. Não misture await com chamadas bloqueantes na mesma função.9
Templates Jinja2
Herança de templates
O sistema de herança do Jinja2 substitui a composição de componentes do React por um modelo mais simples. Um template base define o esqueleto da página. Templates filhos preenchem blocos nomeados:
<!-- base.html — the skeleton -->
<!DOCTYPE html>
<html lang="{{ lang_attr() }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ page_title | default("Blake Crosley") }}</title>
<meta name="description" content="{{ page_description | default('...') }}">
<!-- CSS — single file, no preprocessor -->
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
<!-- JSON-LD structured data -->
<script type="application/ld+json">
{ "@context": "https://schema.org", "@graph": [...] }
</script>
{% block head %}{% endblock %}
</head>
<body>
<header class="header">...</header>
<main id="main" role="main">
{% block content %}{% endblock %}
</main>
<footer class="footer">...</footer>
<!-- Scripts deferred for performance -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>
<!-- pages/about.html — fills the blocks -->
{% extends "base.html" %}
{% block head %}
<script type="application/ld+json">
{ "@type": "AboutPage", "name": "About Blake Crosley", ... }
</script>
{% endblock %}
{% block content %}
<section class="hero">
<h1>About</h1>
<p>Designer, developer, dad.</p>
</section>
{% endblock %}
A diretiva {% extends %} estabelece uma relação pai-filho. O template filho define apenas os blocos que precisa sobrescrever. Todo o resto — o <head>, o header, o footer, as tags de script — vem do base. Isso é composição por subtração, não por construção.
O global asset()
Arquivos estáticos usam versionamento por hash de conteúdo para invalidação de cache:
# cache_assets.py
def build_asset_map(static_dir: Path) -> dict[str, str]:
"""Compute MD5 hashes of all static files at startup."""
asset_map = {}
for filepath in static_dir.rglob("*"):
if filepath.is_file():
rel_path = str(filepath.relative_to(static_dir))
content_hash = hashlib.md5(filepath.read_bytes()).hexdigest()[:10]
asset_map[rel_path] = content_hash
return asset_map
def make_asset_url(asset_map: dict, path: str) -> str:
"""Generate versioned URL: /static/css/styles.css?v=a3f8b2c1d0"""
clean_path = path.lstrip("/")
version = asset_map.get(clean_path, "0")
return f"/static/{clean_path}?v={version}"
No template: {{ asset('css/styles.css') }} renderiza como /static/css/styles.css?v=a3f8b2c1d0. O hash muda quando o arquivo muda, invalidando o cache do CDN. Isso substitui a estratégia de nomes com [contenthash] do webpack por 30 linhas de Python computadas na inicialização.
Include para partials reutilizáveis
Componentes que se repetem em várias páginas usam {% include %}:
<!-- base.html -->
{% include "components/_language_switcher.html" %}
<!-- components/_language_switcher.html -->
{%- set current = current_locale() -%}
{%- set locales = all_locales() -%}
<div class="language-switcher"
x-data="{ open: false }"
@click.away="open = false">
<button @click="open = !open" :aria-expanded="open">
{{ current_locale_native() }}
</button>
<ul class="language-switcher-menu"
:class="{ 'is-open': open }"
x-cloak>
{% for locale in locales %}
<li>
<a href="{{ locale_url(request.url.path, locale.code) }}"
hreflang="{{ locale.code }}">
{{ locale.native }}
</a>
</li>
{% endfor %}
</ul>
</div>
O prefixo underscore (_language_switcher.html) é uma convenção que indica um partial — um fragmento de template que não deve ser renderizado de forma independente. Esse componente usa tanto Alpine.js (para o toggle do dropdown) quanto Jinja2 (para a lista de idiomas). A fronteira é clara: Alpine.js controla o estado abrir/fechar, Jinja2 controla os dados.
Macros para componentes reutilizáveis
Macros são as funções do Jinja2 — blocos de template reutilizáveis com parâmetros:
<!-- components/_macros.html -->
{% macro card(title, description, href, badge=None) %}
<article class="card">
<a href="{{ href }}" class="card__link">
{% if badge %}
<span class="card__badge">{{ badge }}</span>
{% endif %}
<h3 class="card__title">{{ title }}</h3>
{% if description %}
<p class="card__description">{{ description }}</p>
{% endif %}
</a>
</article>
{% endmacro %}
{% macro optimized_image(image_config, loading="lazy") %}
{% if image_config.get("svg") %}
<img src="{{ image_config.svg }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}">
{% else %}
<picture>
<source type="image/webp"
srcset="{{ image_config.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image_config.fallback }}"
width="{{ image_config.width }}"
height="{{ image_config.height }}"
alt="{{ image_config.alt }}"
loading="{{ loading }}">
</picture>
{% endif %}
{% endmacro %}
Importe e use macros nos templates de página:
{% from "components/_macros.html" import card, optimized_image %}
<section class="projects">
{% for project in projects %}
{{ card(
title=project.title,
description=project.description,
href=project.link,
badge="New" if project.is_new else None
) }}
{% endfor %}
</section>
Macros substituem componentes React para padrões de apresentação. Elas aceitam parâmetros, suportam valores padrão e se compõem com outras macros. A diferença: macros renderizam uma vez no servidor e produzem HTML estático. Componentes React renderizam no cliente e mantêm estado. Para exibição de conteúdo, macros são a ferramenta certa.
Contexto de template e globals
Globals do Jinja2 são funções disponíveis em todos os templates sem necessidade de passagem explícita:
# In main.py — register globals
templates.env.globals["asset"] = lambda path: make_asset_url(_asset_map, path)
templates.env.globals["csrf_token"] = generate_csrf_token
templates.env.globals["analytics_script"] = analytics.tracking_script
O global asset() gera URLs versionadas. O global csrf_token() gera tokens CSRF novos. O global analytics_script() injeta o snippet de rastreamento. Essas funções podem ser chamadas em qualquer template sem que o handler da rota precise passá-las explicitamente.
Para i18n, a configuração é mais elaborada — as funções de tradução precisam acessar o idioma da requisição atual:
# i18n/jinja.py
def setup_i18n_jinja(env):
"""Register translation functions as Jinja2 globals."""
env.globals["_"] = get_translation # _('ui.nav.about')
env.globals["locale_prefix"] = get_locale_prefix # '/ja' or ''
env.globals["current_locale"] = get_current_locale
env.globals["all_locales"] = get_all_locales
env.globals["alternate_urls"] = get_alternate_urls
env.globals["lang_attr"] = get_lang_attr # 'ja' for HTML lang
env.globals["og_locale"] = get_og_locale # 'ja_JP' for og:locale
env.globals["jsonld_lang"] = get_jsonld_lang # 'ja-JP' for JSON-LD
Cada função lê o idioma a partir da variável de contexto da requisição definida pelo middleware de localização. O template chama {{ _('ui.nav.about') }} e obtém a string traduzida para o idioma da requisição atual, sem nenhum parâmetro de idioma explícito.
Blocos condicionais
O sistema de blocos do Jinja2 suporta sobrescritas condicionais:
<!-- base.html -->
{% block head %}{% endblock %}
<!-- pages/blog/post.html -->
{% block head %}
<script type="application/ld+json">
{
"@type": "Article",
"headline": "{{ post.meta.title }}",
"author": { "@id": "https://blakecrosley.com/#person" },
"datePublished": "{{ post.meta.date.isoformat() }}",
"dateModified": "{{ post.meta.updated.isoformat() if post.meta.updated else post.meta.date.isoformat() }}"
}
</script>
{% if post.meta.scripts %}
{% for script in post.meta.scripts %}
<script defer src="{{ asset(script.lstrip('/static/')) }}"></script>
{% endfor %}
{% endif %}
{% if post.meta.styles %}
{% for style in post.meta.styles %}
<link rel="stylesheet" href="{{ asset(style.lstrip('/static/')) }}">
{% endfor %}
{% endif %}
{% endblock %}
Posts de blog declaram suas dependências no frontmatter YAML (scripts: ["/static/js/boids.js"]). O template as inclui condicionalmente. Páginas que não precisam de scripts ou estilos extras não carregam nenhum — sem código morto, sem imports não utilizados.
Filtros personalizados
Filtros do Jinja2 transformam dados durante a renderização. O filtro sanitize previne XSS em conteúdo gerado por usuários:
import nh3
ALLOWED_TAGS = {"a", "b", "blockquote", "br", "code", "em", "h1", "h2",
"h3", "h4", "h5", "h6", "hr", "i", "img", "li", "ol",
"p", "pre", "span", "strong", "table", "td", "th", "tr", "ul"}
def sanitize_html(value: str) -> str:
"""Sanitize HTML to prevent XSS attacks."""
if not value:
return ""
return nh3.clean(
value,
tags=ALLOWED_TAGS,
attributes={"a": {"href", "title"}, "img": {"src", "alt"}},
link_rel="noopener noreferrer",
)
templates.env.filters["sanitize"] = sanitize_html
Nos templates: {{ user_content | sanitize }}. A biblioteca nh3 é um sanitizador de HTML baseado em Rust — rápido e seguro. Ela remove qualquer tag ou atributo que não esteja na lista de permitidos, prevenindo XSS persistente mesmo que o conteúdo venha de uma fonte não confiável.10
HTMX em profundidade
HTMX transforma qualquer elemento HTML em algo capaz de emitir requisições HTTP e inserir a resposta no DOM. A sacada principal é arquitetural: HTML renderizado no servidor é a API. O servidor retorna a representação final. Sem renderização no cliente, sem serialização JSON, sem hidratação.
Atributos principais
| Atributo | Finalidade | Exemplo |
|---|---|---|
hx-get |
Emitir requisição GET | hx-get="/search?q=term" |
hx-post |
Emitir requisição POST | hx-post="/contact" |
hx-target |
Onde inserir a resposta | hx-target="#results" |
hx-swap |
Como inserir a resposta | hx-swap="innerHTML" (padrão), outerHTML, beforeend |
hx-trigger |
O que dispara a requisição | hx-trigger="click", keyup changed delay:300ms, load |
hx-indicator |
Elemento exibido durante a requisição | hx-indicator="#spinner" |
hx-push-url |
Atualizar a URL do navegador | hx-push-url="true" |
hx-replace-url |
Substituir URL sem entrada no histórico | hx-replace-url="true" |
Padrão 1: Quiz interativo (estado multi-etapas no servidor)
blakecrosley.com inclui um quiz interativo que guia os usuários pela seleção de ferramentas. Todo o estado do quiz fica no servidor — sem gerenciamento de estado no cliente:
<!-- _quiz_container.html — carregamento inicial -->
<div hx-get="/api/quiz/claude-vs-codex/step?answers="
hx-trigger="load"
hx-swap="innerHTML"
id="quiz-wrapper">
<p>Loading quiz...</p>
</div>
<!-- _quiz_step.html — cada pergunta -->
<div class="quiz-step" id="quiz-container">
<p>Question {{ step }} of {{ total }}</p>
<h3>{{ question.question }}</h3>
<div class="quiz-step__options">
{% for opt in question.options %}
<button class="quiz-step__btn"
hx-get="/api/quiz/claude-vs-codex/step?answers={{ answers }},{{ opt.value }}"
hx-target="#quiz-container"
hx-swap="outerHTML">
{{ opt.label }}
</button>
{% endfor %}
</div>
</div>
Cada clique em um botão envia as respostas acumuladas como parâmetro de consulta. O servidor calcula a próxima pergunta ou o resultado final com base no histórico de respostas. O estado se acumula na URL — sem cookies, sem sessões, sem JavaScript no cliente. O quiz avança por meio de swaps outerHTML: cada resposta substitui o elemento inteiro da etapa do quiz.
Padrão 2: Lista de posts paginada
A página de artigos usa HTMX para uma paginação fluida que atualiza a URL:
<!-- Link de paginação -->
<a href="/writing?page=2&category=Engineering"
hx-get="/writing?page=2&category=Engineering"
hx-target="#writing-content"
hx-swap="innerHTML"
hx-replace-url="true"
hx-indicator="#writing-loading"
aria-label="Go to page 2">
2
</a>
Quatro atributos trabalhando juntos:
hx-getemite a requisição para a mesma URL dohref(aprimoramento progressivo — funciona sem JavaScript)hx-targetinsere a resposta no container#writing-contenthx-replace-url="true"atualiza a URL do navegador sem adicionar uma entrada no históricohx-indicatorexibe um spinner de carregamento durante a requisição
O servidor detecta requisições HTMX pelo header HX-Request e retorna apenas o fragmento da lista de posts em vez da página completa. Por isso o middleware de headers de segurança adiciona Vary: HX-Request — para que caches de CDN armazenem a página completa e o fragmento separadamente.11
Padrão 3: Busca com debounce
<input type="search" name="q"
hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#results"
hx-indicator="#search-spinner" />
<div id="results"></div>
O atributo hx-trigger combina três modificadores:
keyupdispara ao soltar a teclachangeddispara apenas se o valor realmente mudou (evita requisições duplicadas por teclas modificadoras)delay:300msaplica debounce — aguarda 300ms após o último keyup antes de disparar
O servidor retorna um fragmento HTML renderizado:
@router.get("/api/search")
async def search(request: Request, q: str = ""):
results = search_content(q)
return templates.TemplateResponse("components/_search_results.html", {
"request": request,
"results": results,
"query": q,
})
Sem estado no cliente. Sem biblioteca de debounce. Sem useEffect. O template renderiza os resultados, HTMX faz o swap no DOM, e o servidor é a única fonte de verdade.
Padrão 4: Swaps Out-of-Band (OOB)
Às vezes uma única ação do servidor precisa atualizar múltiplos elementos no DOM. O mecanismo de swap out-of-band do HTMX resolve isso sem orquestração no cliente:
<!-- O servidor retorna múltiplos elementos em uma única resposta -->
<!-- Alvo principal: swap normal via hx-target -->
<div id="cart-items">
<ul>
<li>Widget A — $29.99</li>
<li>Widget B — $14.99</li>
</ul>
</div>
<!-- Alvo OOB: swap independente via hx-swap-oob -->
<span id="cart-count" hx-swap-oob="true">2 items</span>
<span id="cart-total" hx-swap-oob="true">$44.98</span>
O atributo hx-swap-oob="true" diz ao HTMX para encontrar o elemento pelo id em qualquer lugar do DOM e substituí-lo, independentemente do hx-target. Isso substitui o padrão “elevar o estado” do React — o servidor calcula todo o estado derivado e envia o HTML final de cada elemento em uma única resposta.
Um formulário de contato demonstra isso bem: enviar o formulário pode substituir o corpo do formulário por uma mensagem de sucesso e, ao mesmo tempo, atualizar um badge de notificação via OOB swap:
Padrão 5: Links com boost
HTMX pode aplicar “boost” em links de navegação padrão para usar AJAX em vez de carregamentos de página completos:
<nav hx-boost="true">
<a href="/about">About</a>
<a href="/writing">Writing</a>
<a href="/guides">Guides</a>
</nav>
Com hx-boost="true", clicar em um link busca a página via AJAX, faz o swap do conteúdo do <body> e atualiza a URL — sem recarregamento completo da página. O histórico do navegador funciona normalmente (botões voltar/avançar). Se o JavaScript falhar, os links funcionam como navegação padrão.
O benefício é a performance percebida: a navegação com boost parece instantânea porque o navegador não precisa re-analisar o CSS, re-avaliar scripts ou re-renderizar o layout. Apenas o conteúdo do <body> muda. Links com boost funcionam bem para elementos de navegação principal, o que faz as transições de página parecerem uma single-page application sem a arquitetura de SPA.
Padrão 6: Headers de requisição do HTMX
HTMX envia headers customizados em cada requisição:
| Header | Valor | Caso de uso |
|---|---|---|
HX-Request |
true |
Detectar requisições HTMX no servidor |
HX-Target |
ID do elemento | Saber qual elemento receberá a resposta |
HX-Trigger |
ID do elemento | Saber qual elemento disparou a requisição |
HX-Current-URL |
URL completa | Saber a página atual do usuário |
O servidor pode usar HX-Request para retornar respostas diferentes:
@router.get("/writing")
async def writing(request: Request, page: int = 1, category: str = None):
posts = load_all_posts(page=page, category=category)
context = {"request": request, "posts": posts, "current_page": page}
# HTMX request: return only the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html", context
)
# Normal request: return the full page
return templates.TemplateResponse("pages/writing/index.html", context)
Esse padrão de resposta dual é central na arquitetura. Um carregamento de página completo retorna o documento inteiro (template base + conteúdo da página). Uma navegação HTMX retorna apenas o conteúdo alterado. O servidor decide, não o cliente.
Padrão 7: Aprimoramento progressivo
Todo link HTMX em blakecrosley.com inclui um atributo href padrão:
<a href="/writing?page=2"
hx-get="/writing?page=2"
hx-target="#writing-content"
hx-swap="innerHTML">
Next Page
</a>
Se o JavaScript não carregar, o href funciona como um link normal. Se o HTMX carregar, ele intercepta o clique e realiza um swap via AJAX. Isso é aprimoramento progressivo: o site funciona sem JavaScript, e o HTMX melhora a experiência quando disponível.
Padrão 8: Estados de carregamento
<button hx-post="/api/contact"
hx-target="#form-result"
hx-indicator="#submit-spinner">
<span id="submit-spinner" class="htmx-indicator">Sending...</span>
<span>Send Message</span>
</button>
HTMX adiciona a classe htmx-request ao elemento que disparou a requisição durante o processamento. O atributo hx-indicator aponta para um elemento que se torna visível durante a requisição. Estilize com CSS:
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator {
display: inline;
}
Sem gerenciamento de estado de carregamento. Sem useState(false). Sem setLoading(true). CSS cuida da visibilidade, HTMX cuida do toggle de classe.
Padrões do Alpine.js
O Alpine.js preenche a lacuna que o HTMX deixa: estado que existe apenas no cliente e nunca precisa ir ao servidor. Se o usuário clica em um dropdown e ele abre, esse estado existe apenas no navegador. O Alpine.js gerencia isso com atributos HTML.
A regra de fronteira
A fronteira entre HTMX e Alpine.js é precisa:
| Tipo de estado | Ferramenta | Exemplo |
|---|---|---|
| Precisa de dados do servidor | HTMX | Resultados de busca, validação de formulário, paginação |
| Existe apenas no navegador | Alpine.js | Abrir/fechar dropdown, toggle de menu mobile, visibilidade de modal |
| Combina ambos | Ambos | Seletor de idioma (toggle com Alpine.js, navegação com HTMX) |
Navegação mobile
O template base envolve todo o cabeçalho em um componente Alpine.js:
<div x-data="{ navOpen: false, langOpen: false }"
@keydown.escape.window="navOpen = false; langOpen = false">
<!-- Mobile hamburger button -->
<button @click="navOpen = !navOpen; langOpen = false"
:aria-expanded="navOpen"
:class="navOpen ? 'nav__toggle is-open' : 'nav__toggle'"
aria-label="Toggle navigation">
<span class="nav__toggle-icon">
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
<span class="nav__toggle-bar"></span>
</span>
</button>
<!-- Mobile menu panel -->
<div class="mobile-menu" x-show="navOpen" x-cloak>
<nav class="mobile-menu__nav">
<a href="/about" @click="navOpen = false">About</a>
<a href="/#work" @click="navOpen = false">Work</a>
<a href="/writing" @click="navOpen = false">Writing</a>
</nav>
</div>
</div>
Padrões-chave do Alpine.js:
x-datadeclara o escopo do componente e o estado inicialx-showalterna a visibilidade com base no estado (usa CSSdisplay: none)x-cloakoculta o elemento até o Alpine.js inicializar (evita o flash de conteúdo sem estilo)@clickvincula handlers de clique com expressões:aria-expanded(atalho parax-bind:aria-expanded) define atributos dinamicamente@keydown.escape.windowescuta a tecla Escape globalmente para fechar painéis
Componente dropdown
O seletor de idioma usa Alpine.js para o estado de toggle com @click.away para fechar ao clicar fora:
<div x-data="{ open: false }"
@click.away="open = false"
@keydown.escape.window="open = false">
<button @click="open = !open"
:aria-expanded="open"
aria-haspopup="listbox">
English
<svg :class="{ 'rotated': open }">...</svg>
</button>
<ul :class="{ 'is-open': open }"
:aria-hidden="!open"
role="listbox"
x-cloak>
<li role="option">
<a href="/ja/about">日本語</a>
</li>
<!-- more languages -->
</ul>
</div>
O modificador @click.away fecha o dropdown ao clicar fora dele. O Alpine.js resolve isso com um único atributo — sem registro de event listeners, sem cleanup, sem gerenciamento de refs.
Quando usar Alpine.js vs. JavaScript puro
O Alpine.js é adequado quando:
- O estado é limitado a um único elemento DOM (dropdown, modal, toggle)
- As interações são binárias ou simples (abrir/fechar, mostrar/ocultar, alternar)
- Múltiplos elementos precisam reagir à mesma mudança de estado
- Atributos de acessibilidade devem permanecer sincronizados com a visibilidade
JavaScript puro é adequado quando:
- A interação envolve computação complexa (visualizações, simulações)
- O componente tem seu próprio loop de renderização (canvas, animação)
- Performance é crítica (o Alpine.js adiciona overhead por componente
x-data) - A lógica excede 20-30 linhas de expressões Alpine.js
O blakecrosley.com usa Alpine.js para navegação, troca de idioma e toggles de conteúdo. Os 20 componentes interativos do blog (simulação de boids, visualizador de código Hamming, etc.) usam JavaScript puro porque exigem renderização em canvas e máquinas de estado complexas.
Exemplo completo: filtragem por categoria em /writing
Esta seção acompanha um recurso real do código de produção por todas as camadas: rota, template, interação HTMX, segurança, cache e resultado renderizado. O recurso: abas de categoria na página de artigos que filtram posts do blog sem recarregar a página inteira.
A rota (app/routes/pages.py:508)
async def writing_listing(request: Request, page: int = 1, category: str | None = None):
"""Writing page — blog posts and external publications."""
templates = get_templates(request)
markdown_posts = load_all_posts(published_only=True)
all_posts = CUSTOM_BLOG_POSTS + markdown_posts
# Filter by category if specified
if category and category in CATEGORY_MAP:
display_name = CATEGORY_MAP[category]
all_posts = [
p for p in all_posts
if _get_post_category(p).lower() == display_name.lower()
]
# Pagination
total_pages = max(1, (len(all_posts) + POSTS_PER_PAGE - 1) // POSTS_PER_PAGE)
page = max(1, min(page, total_pages))
paginated = all_posts[(page - 1) * POSTS_PER_PAGE : page * POSTS_PER_PAGE]
template_context = {
"request": request,
"posts": paginated,
"categories": categories,
"current_category": category,
"current_page": page,
"total_pages": total_pages,
# ... SEO: canonical, prev/next URLs
}
# HTMX partial: return just the post list fragment
if request.headers.get("HX-Request"):
return templates.TemplateResponse(
"pages/writing/_post_list.html",
template_context,
)
# Full page for direct navigation
return templates.TemplateResponse(
"pages/writing/index.html",
template_context,
)
A verificação do header HX-Request é o padrão central: mesma rota, mesmos dados, template diferente. O HTMX recebe um fragmento. Navegadores recebem a página completa.
As abas de categoria (HTMX)
<!-- Category filter tabs -->
<nav class="writing-categories">
<a href="/writing"
hx-get="/writing"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if not current_category %}active{% endif %}">
All ({{ total_posts }})
</a>
{% for cat in categories %}
<a href="/writing?category={{ cat.slug }}"
hx-get="/writing?category={{ cat.slug }}"
hx-target="#post-list"
hx-push-url="true"
class="category-tab {% if current_category == cat.slug %}active{% endif %}">
{{ cat.name }} ({{ cat.count }})
</a>
{% endfor %}
</nav>
<div id="post-list">
{% include "pages/writing/_post_list.html" %}
</div>
Cada aba tem tanto href (funciona sem JavaScript) quanto hx-get (troca apenas a lista de posts). O hx-push-url atualiza a URL do navegador para que a visualização filtrada possa ser compartilhada e favoritada.
O partial (pages/writing/_post_list.html)
O partial renderiza de forma idêntica, seja incluído no carregamento da página ou trocado pelo HTMX:
{% for post in posts %}
<article class="post-card">
<a href="{{ locale_prefix() }}/blog/{{ post.meta.slug }}">
<h3>{{ post.meta.title }}</h3>
<p>{{ post.meta.description }}</p>
<time>{{ post.meta.date }}</time> · {{ post.reading_time }}m
</a>
</article>
{% endfor %}
Nenhuma marcação especial do HTMX no partial. Nenhuma lógica de renderização no cliente. O mesmo HTML funciona tanto para o carregamento inicial da página quanto para cada filtro subsequente.
Segurança
Os valores de categoria são validados contra CATEGORY_MAP (um dicionário no servidor) antes da filtragem. Categorias inválidas são ignoradas, não ecoadas de volta. Nenhuma entrada do usuário é interpolada em SQL ou HTML. O header CSP bloqueia scripts inline.
Cache
As respostas de categoria são dinâmicas (sem cache no CDN). Porém, os assets estáticos (CSS, HTMX, Alpine.js) possuem hash de conteúdo e são cacheados indefinidamente após o primeiro carregamento. Trocas de categoria subsequentes transferem apenas o partial HTML (~3-5KB) — sem CSS, sem JS, sem re-download de imagens.
O que isso demonstra
Um recurso, código real de produção, zero ferramentas de build. O servidor filtra e renderiza HTML. O HTMX troca a lista de posts. O Alpine.js não está envolvido (nenhum estado no cliente é necessário). A URL é atualizada para compartilhamento. Progressive enhancement: as abas funcionam como links comuns sem JavaScript. Total de JavaScript customizado para este recurso: zero linhas.
Extensões opcionais
As seções a seguir cobrem padrões que complementam o stack principal, mas não são usados no blakecrosley.com. Estão incluídas porque representam as adições mais comuns que equipes fazem ao adotar esta arquitetura.
Bootstrap 5 sem Sass
Nota: blakecrosley.com usa CSS puro com propriedades customizadas — sem Bootstrap. Esta seção aborda o Bootstrap 5 como uma opção para equipes que desejam um framework utilitário sem etapa de build. O CSS compilado do Bootstrap pode ser carregado de um CDN ou incluído na sua folha de estilos. Os padrões abaixo são genéricos e funcionam junto com a abordagem HTMX + Alpine.js descrita nas seções anteriores.
O Bootstrap 5 removeu a dependência do jQuery e suporta uso autônomo de CSS. Você não precisa de Sass, PostCSS ou qualquer ferramenta de build para usar o sistema de grid e as classes utilitárias do Bootstrap.
Self-hosting sem CDN
blakecrosley.com hospeda localmente todas as bibliotecas de terceiros:
<!-- base.html — no CDN, no external requests -->
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
O self-hosting elimina dependências externas, evita que quedas de CDN quebrem o site e permite cache imutável com URLs baseadas em hash de conteúdo. Baixe o CSS compilado do Bootstrap (não o código-fonte Sass) e coloque-o em static/css/vendor/.
Sistema de grid
O grid do Bootstrap funciona com classes HTML puras:
<div class="container">
<div class="row">
<div class="col-12 col-md-8">
<article>Main content</article>
</div>
<div class="col-12 col-md-4">
<aside>Sidebar</aside>
</div>
</div>
</div>
Sem mixins Sass. Sem @include make-col(). O CSS compilado já inclui as classes responsivas do grid. Para breakpoints personalizados além dos padrões do Bootstrap, escreva media queries em CSS puro.
Sobrescrevendo com CSS puro
Sobrescreva os padrões do Bootstrap com propriedades customizadas de CSS e seletores padrão:
/* Custom design tokens — no Sass, no Tailwind */
:root {
--color-bg-dark: #000000;
--color-text-primary: #ffffff;
--color-text-secondary: rgba(255, 255, 255, 0.65);
--color-text-tertiary: rgba(255, 255, 255, 0.40);
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--spacing-lg: 2rem;
--gutter: 48px;
--font-size-lg: 1.25rem;
}
/* Responsive override — the browser reads this at runtime */
@media (max-width: 768px) {
:root {
--gutter: var(--spacing-md); /* 48px → 24px on mobile */
}
}
/* Override Bootstrap's default body styles */
body {
background: var(--color-bg-dark);
color: var(--color-text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
}
Propriedades customizadas de CSS cascateiam pelo DOM, herdam de elementos pai e respondem a media queries em tempo de execução. Variáveis Sass são compiladas em valores estáticos e desaparecem. Essa distinção é importante para temas: uma única alteração em uma propriedade customizada pode atualizar todos os valores derivados sem recompilação.12
Classes utilitárias vs. CSS de componente
Use classes utilitárias do Bootstrap para espaçamento e layout pontuais. Use CSS de componente para padrões repetidos:
<!-- Bootstrap utility for one-off spacing -->
<div class="mt-4 mb-3 px-2">One-off layout</div>
<!-- Component class for repeated patterns -->
<article class="writing__item">
<h3 class="writing__item-title">Post Title</h3>
<p class="writing__item-description">Description</p>
</article>
/* Component CSS — BEM naming, reusable */
.writing__item {
padding: var(--spacing-md);
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background 0.15s ease;
}
.writing__item:hover {
background: rgba(255, 255, 255, 0.03);
}
.writing__item-title {
font-size: var(--font-size-lg);
margin-bottom: 0.5rem;
}
O princípio: utilitários do Bootstrap para mecânicas de layout (margin, padding, flexbox). CSS customizado para identidade visual (cores, tipografia, animações). Nunca misture classes utilitárias com estilização de componente para a mesma finalidade.
i18n e localização
blakecrosley.com serve conteúdo em 10 idiomas: inglês, japonês, coreano, chinês simplificado, chinês tradicional, alemão, francês, espanhol, polonês e português (brasileiro).
Roteamento de locale baseado em URL
O locale fica no caminho da URL: /about (inglês), /ja/about (japonês), /zh-Hans/about (chinês simplificado). O inglês é o padrão e não possui prefixo.
# i18n/config.py
SUPPORTED_LOCALES = [
"en", "zh-Hans", "zh-Hant", "fr", "de", "ja", "ko", "pl", "pt-BR", "es"
]
DEFAULT_LOCALE = "en"
O middleware de locale extrai o locale do caminho da URL:
# i18n/middleware.py
class LocaleMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
path = request.url.path
# Check if path starts with a supported locale
for locale in SUPPORTED_LOCALES:
if path.startswith(f"/{locale}/") or path == f"/{locale}":
request.state.locale = locale
# Strip locale prefix for route matching
request.scope["path"] = path[len(f"/{locale}"):]
break
else:
request.state.locale = DEFAULT_LOCALE
response = await call_next(request)
return response
O middleware remove o prefixo do locale antes do roteamento. Isso significa que os handlers de rota não precisam de caminhos específicos por locale — /about atende tanto ao inglês (/about) quanto ao japonês (/ja/about) porque o middleware normaliza o caminho.
Funções de tradução nos templates
As variáveis globais do Jinja2 fornecem funções de tradução:
<!-- Template usage -->
<h3>{{ _('ui.footer.navigate') | default('Navigate') }}</h3>
<a href="{{ locale_prefix() }}/about">
{{ _('ui.nav.about') | default('About') }}
</a>
A função _() busca uma chave de tradução no cache em memória. O filtro | default() fornece o fallback em inglês caso a tradução esteja ausente. A função locale_prefix() retorna o prefixo de URL para o locale atual ("" para inglês, "/ja" para japonês).
Tags hreflang
Toda página inclui tags hreflang para todos os locales suportados:
<!-- Generated in base.html -->
{% for alt in alternate_urls(request.url.path) %}
<link rel="alternate" hreflang="{{ alt.hreflang }}" href="{{ alt.url }}">
{% endfor %}
Isso produz:
<link rel="alternate" hreflang="en" href="https://blakecrosley.com/about">
<link rel="alternate" hreflang="ja" href="https://blakecrosley.com/ja/about">
<link rel="alternate" hreflang="zh-Hans" href="https://blakecrosley.com/zh-Hans/about">
<!-- ... all 10 locales -->
<link rel="alternate" hreflang="x-default" href="https://blakecrosley.com/about">
Os mecanismos de busca usam o hreflang para exibir a versão correta do idioma nos resultados de pesquisa. A entrada x-default aponta para a versão em inglês como fallback.13
Armazenamento de traduções e cache em memória
As traduções são armazenadas no Cloudflare D1 (SQLite na edge) e carregadas em um cache em memória através do handler lifespan:
@asynccontextmanager
async def lifespan(app: FastAPI):
# Load translations into memory at startup
for locale in SUPPORTED_LOCALES:
data = await fetch_translations(locale)
TRANSLATIONS[locale] = data
yield
app = FastAPI(lifespan=lifespan)
O cache em memória evita consultas ao banco de dados em cada renderização de página. Atualizações de tradução exigem uma atualização do cache (disparada via endpoint administrativo ou um deploy). Essa arquitetura troca atualidade por performance — traduções mudam com pouca frequência, mas renderizações de página acontecem a cada requisição.
Monitoramento de saúde
blakecrosley.com inclui um endpoint de health check para i18n que monitora a cobertura de tradução por locale:
@app.get("/health/i18n")
async def health_i18n():
cache = get_translation_cache()
result = {
"status": "healthy",
"cache_loaded": cache.is_loaded,
"locales": {},
"alerts": [],
}
# Check coverage for each locale
for locale in SUPPORTED_LOCALES:
coverage = await calculate_coverage(locale, en_count)
result["locales"][locale] = {"coverage": round(coverage, 2)}
if coverage < 99.5:
result["alerts"].append(
f"{locale}: {coverage:.1f}% coverage (threshold: 99.5%)"
)
result["status"] = "warning"
return result
O limite de 99,5% de cobertura detecta traduções ausentes antes que os usuários encontrem strings não traduzidas. O endpoint de saúde se integra ao monitoramento do Railway para alertar quando a cobertura cai — por exemplo, após adicionar novas strings de UI que ainda não foram traduzidas.
Renderização de conteúdo com reconhecimento de locale
Posts de blog e guias suportam traduções por locale de metadados e conteúdo:
# In route handler
translated = get_blog_translation(post.meta.slug, locale)
return templates.TemplateResponse("pages/blog/post.html", {
"request": request,
"post": post,
"translated_title": translated.title if translated else post.meta.title,
"translated_description": translated.description if translated else post.meta.description,
})
<!-- In template -->
<h1>{{ translated_title }}</h1>
<p class="post__description">{{ translated_description }}</p>
<!-- Body content falls back to English if translation unavailable -->
{{ post.html | sanitize | safe }}
O padrão é consistente: tente o conteúdo traduzido primeiro, use o inglês como fallback. Isso permite tradução parcial — um usuário japonês vê títulos e descrições traduzidos mesmo que o corpo completo do artigo permaneça em inglês. O filtro | default() do Jinja2 codifica esse padrão em um único pipe:
{{ translated.title if translated else post.meta.title }}
Tradução de dados de locale
Conteúdo estático como descrições de projetos e labels de navegação são traduzidos através de funções auxiliares que mantêm a mesma estrutura de dados enquanto substituem as strings específicas do locale:
# i18n/data.py
def translate_projects(projects: list, locale: str) -> list:
"""Return projects with translated titles and descriptions."""
if locale == "en":
return projects
translated = []
for project in projects:
t = get_translation(f"project.{project['slug']}.title", locale)
d = get_translation(f"project.{project['slug']}.description", locale)
translated.append({
**project,
"title": t or project["title"],
"description": d or project["description"],
})
return translated
Essa abordagem mantém a camada de tradução separada da camada de dados. As rotas passam a mesma lista projects independentemente do locale. As funções de tradução envolvem os dados de forma transparente.
Sitemap com alternativas hreflang
O sitemap dinâmico inclui todas as páginas em todos os locales com referências cruzadas:
@app.get("/sitemap.xml")
async def sitemap():
for page in static_pages:
for locale in SUPPORTED_LOCALES:
# Each URL entry includes alternates for all locales
locale_path = f"/{locale}{path}" if locale != "en" else path
xml_parts.append(f"<loc>{base_url}{locale_path}</loc>")
# Add xhtml:link alternates
for alt_locale in SUPPORTED_LOCALES:
alt_path = f"/{alt_locale}{path}" if alt_locale != "en" else path
hreflang = LOCALE_TO_HREFLANG[alt_locale]
xml_parts.append(
f'<xhtml:link rel="alternate" hreflang="{hreflang}" '
f'href="{base_url}{alt_path}"/>'
)
Isso produz 10 entradas de URL por página (uma por locale), cada uma com 11 links alternativos (10 locales + x-default). Para um site com 50 páginas, o sitemap contém 500 entradas de URL com 5.500 links hreflang. O sitemap é gerado dinamicamente e armazenado em cache por uma hora.
Padrões de banco de dados
Observação: blakecrosley.com usa Cloudflare D1 (SQLite serverless) via HTTP para todos os dados persistentes, não SQLAlchemy. Esta seção aborda o padrão async do SQLAlchemy para projetos FastAPI que precisam de um banco de dados relacional — a configuração de produção mais comum para esta stack.
SQLAlchemy 2.0 async
Para aplicações que precisam de um banco de dados relacional, o suporte async do SQLAlchemy 2.0 se integra bem ao FastAPI:
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker, DeclarativeBase
engine = create_async_engine("sqlite+aiosqlite:///./data.db")
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
class Base(DeclarativeBase):
pass
Observação de instalação (SQLAlchemy 2.0.50+): a partir da versão 2.0.50, a dependência greenlet da stack async não é mais instalada por padrão. Use o extra asyncio para que ela seja incluída, ou o primeiro await contra o engine falhará com um erro de greenlet ausente:23
pip install "sqlalchemy[asyncio]" aiosqlite
O SQLAlchemy 2.0.50 também exige Python 3.10+ (3.7–3.9 foram removidos) e adiciona wheels para free-threaded (3.13t).23
Dependency injection para sessões de banco de dados
from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async with async_session() as session:
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
@router.get("/users/{user_id}")
async def get_user(request: Request, user_id: int, db: AsyncSession = Depends(get_db)):
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise HTTPException(404, "User not found")
return templates.TemplateResponse("pages/user.html", {
"request": request, "user": user
})
A dependência get_db gerencia o ciclo de vida da sessão: ela abre uma sessão, a entrega ao route handler, faz commit em caso de sucesso e rollback em caso de exceção. Toda operação de banco de dados usa queries parametrizadas — nunca interpolação de strings.
Integração com Pydantic
Modelos Pydantic validam a entrada na fronteira do API e serializam a saída para templates:
from pydantic import BaseModel, EmailStr
class ContactForm(BaseModel):
name: str
email: EmailStr
message: str
@router.post("/contact")
async def submit_contact(request: Request, form: ContactForm):
# form.name, form.email, form.message are validated
await send_email(form)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
O Pydantic valida tipos, formatos (email, URL) e restrições (comprimento mínimo/máximo) antes que o route handler seja executado. Entradas inválidas retornam automaticamente uma resposta 422. Isso substitui bibliotecas de validação de formulário no lado do cliente — o servidor valida, e HTMX troca pelo aviso de sucesso ou pelo feedback de erro.
Migrations com Alembic
Alembic gerencia mudanças no schema do banco de dados:
# Generate a migration from model changes
alembic revision --autogenerate -m "add user preferences table"
# Apply migrations
alembic upgrade head
# Roll back one migration
alembic downgrade -1
O recurso de autogenerate compara os modelos SQLAlchemy com o schema atual do banco de dados e gera scripts de migration. Esses scripts são arquivos versionados Python que vivem no repositório:
# alembic/versions/001_add_user_preferences.py
def upgrade():
op.create_table(
"user_preferences",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("user_id", sa.Integer, sa.ForeignKey("users.id")),
sa.Column("locale", sa.String(10), default="en"),
sa.Column("theme", sa.String(20), default="dark"),
)
def downgrade():
op.drop_table("user_preferences")
As migrations rodam durante o deployment (antes de a aplicação iniciar). Isso garante que o schema do banco de dados corresponda ao código da aplicação. Para blakecrosley.com, a maior parte dos dados vive no Cloudflare D1 (acessado via HTTP), então as migrations do Alembic se aplicam ao banco de dados local SQLite ou PostgreSQL usado para dados de sessão e analytics.
O padrão Cloudflare D1
blakecrosley.com usa Cloudflare D1 como um banco de dados remoto acessado por meio de um proxy Cloudflare Worker:
class D1Client:
"""HTTP client for Cloudflare D1 via Worker proxy."""
def __init__(self, worker_url: str, auth_secret: str):
self.worker_url = worker_url
self.auth_secret = auth_secret
async def fetch_all(self, sql: str, params: list = None) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.worker_url}/query",
json={"sql": sql, "params": params or []},
headers={"Authorization": f"Bearer {self.auth_secret}"},
)
return response.json()["results"]
Esse padrão funciona para aplicações que precisam de um banco de dados, mas não querem gerenciar um servidor de banco de dados. D1 é SQLite na edge da Cloudflare, acessado via HTTP. O proxy Worker cuida da autenticação e do rate limiting. O trade-off é a latência: cada query é uma requisição HTTP (~50-100ms), em vez de uma conexão local com o banco de dados (~1-5ms). O cache em memória na inicialização reduz esse impacto em workloads com muitas leituras, como traduções.
Segurança
Limite de tamanho do corpo da requisição
O Starlette 1.6.0 adicionou max_body_size, o controle que faltava a esta stack: sem ele, um cliente pode transmitir um corpo sem limite para seu app e fazer da memória o ponto de falha. Defina-o em Starlette, Router, Mount ou em uma Route individual, ou envolva qualquer app ASGI em RequestBodyLimitMiddleware. Rotas aninhadas podem aumentar ou reduzir o limite de todo o app, então um endpoint de upload pode ser permissivo enquanto todo o restante permanece restrito.
app = FastAPI(lifespan=lifespan)
app.router.max_body_size = 2 * 1024 * 1024 # 2 MB default for the whole app
O limite contabiliza os bytes realmente recebidos do servidor ASGI, incluindo dados de arquivo multipart, e trata Content-Length apenas como uma verificação de falha antecipada — um cabeçalho ausente ou com valor menor não consegue contorná-lo. O padrão é None, ou seja, sem limite, portanto este recurso é opt-in.28
Middleware de cabeçalhos de segurança
blakecrosley.com implementa cabeçalhos de segurança reforçados por meio de middleware personalizado:
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
CSP_DIRECTIVES = {
"default-src": "'self'",
"script-src": "'self' 'unsafe-inline' 'unsafe-eval'",
"style-src": "'self' 'unsafe-inline'",
"img-src": "'self' data: https:",
"connect-src": "'self'",
"frame-ancestors": "'self'",
"base-uri": "'self'",
"form-action": "'self'",
"upgrade-insecure-requests": "",
}
async def dispatch(self, request, call_next):
response = await call_next(request)
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "SAMEORIGIN"
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
response.headers["Strict-Transport-Security"] = (
"max-age=31536000; includeSubDomains"
)
response.headers["Cross-Origin-Opener-Policy"] = "same-origin"
response.headers["Content-Security-Policy"] = self.csp
response.headers["Permissions-Policy"] = self.PERMISSIONS_POLICY
return response
A CSP inclui 'unsafe-inline' e 'unsafe-eval' porque Alpine.js precisa deles para avaliar expressões. A alternativa é a build compatível com CSP do Alpine.js, que tem limitações.14 Todos os demais recursos ficam restritos: frame-ancestors impede clickjacking, form-action limita envios de formulário à mesma origem e upgrade-insecure-requests força HTTPS.
Segurança do cache de CDN com HTMX
O middleware de cabeçalhos de segurança adiciona Vary: HX-Request às respostas de HTMX:
if request.headers.get("HX-Request"):
existing_vary = response.headers.get("Vary", "")
if "HX-Request" not in existing_vary:
parts = [v.strip() for v in existing_vary.split(",") if v.strip()]
parts.append("HX-Request")
response.headers["Vary"] = ", ".join(parts)
Sem esse cabeçalho, uma CDN poderia armazenar em cache uma resposta de fragmento de HTMX e entregá-la como página completa a uma requisição que não é de HTMX (ou vice-versa). O cabeçalho Vary instrui a CDN a armazenar entradas de cache separadas com base no valor do cabeçalho HX-Request.11
Proteção contra CSRF
Formulários de HTMX usam tokens CSRF sem estado e assinados com HMAC:
# csrf.py
def generate_csrf_token() -> str:
"""Token format: timestamp:random:HMAC-SHA256-signature"""
timestamp = str(int(time.time()))
random_value = secrets.token_hex(16)
payload = f"{timestamp}:{random_value}"
signature = hmac.new(
CSRF_SECRET.encode(), payload.encode(), hashlib.sha256
).hexdigest()
return f"{payload}:{signature}"
def validate_csrf_token(token: str) -> bool:
"""Verify signature and check expiration (1 hour)."""
timestamp, random_value, signature = token.split(":")
if int(time.time()) - int(timestamp) > 3600:
return False
expected = hmac.new(
CSRF_SECRET.encode(),
f"{timestamp}:{random_value}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
O token é gerado no template por meio de uma global Jinja2 e incluído nas requisições de formulário de HTMX:
<form hx-post="/contact" hx-target="#form-result">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<!-- form fields -->
</form>
Tokens sem estado eliminam o armazenamento de sessão no servidor. A assinatura HMAC garante que o token foi gerado pelo servidor. O timestamp impede ataques de repetição. hmac.compare_digest impede ataques de temporização.15
Sanitização de HTML
Conteúdo gerado por usuários passa por nh3 antes de ser renderizado:
templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}
A biblioteca nh3 remove tags e atributos que não estão na allowlist. Os links recebem automaticamente rel="noopener noreferrer". Essa defesa é independente da CSP — ela impede XSS armazenado na camada de renderização, enquanto a CSP impede scripts injetados na camada do navegador. Defesa em profundidade.
Validação de entrada
Os modelos Pydantic validam todas as entradas na fronteira de API:
from pydantic import BaseModel, Field, EmailStr
class ContactRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=100)
email: EmailStr
message: str = Field(..., min_length=10, max_length=5000)
FastAPI retorna automaticamente 422 Unprocessable Entity para entradas inválidas. Combinado com consultas parametrizadas ao banco de dados (o SQLAlchemy nunca interpola strings), isso impede injeção de SQL e garante segurança de tipos nas fronteiras.
Desempenho
Lighthouse 100/100/100/100
blakecrosley.com obtém 100 nas quatro categorias do Lighthouse: Performance, Accessibility, Best Practices e SEO. Verifique em PageSpeed Insights.2
As principais otimizações:
Estratégia de carregamento de CSS
blakecrosley.com carrega CSS com uma única tag <link> e URLs com hash de conteúdo para cache imutável:
<link rel="stylesheet" href="{{ asset('css/styles.css') }}">
O helper asset() acrescenta um hash de conteúdo (?v=a3b2c1d4), para que o navegador armazene o arquivo em cache indefinidamente até que o conteúdo mude. Sem extração de CSS crítico, sem truque de mídia de impressão, sem carregamento baseado em JavaScript. O arquivo de CSS tem cerca de 8 KB compactado com gzip — pequeno o suficiente para que a abordagem de uma única requisição alcance 100 no Lighthouse Performance sem malabarismos de otimização.
Compressão GZip
app.add_middleware(GZipMiddleware, minimum_size=500)
Respostas com mais de 500 bytes são compactadas, exceto os tipos de conteúdo excluídos por padrão que o Starlette 1.5.0 introduziu (arquivos compactados, imagens, áudio, vídeo, fontes, SSE). HTML compacta de 70 a 80%, reduzindo um documento de 15 KB para 3 a 4 KB.28
Cache imutável de assets estáticos
# In security headers middleware
if request.url.path.startswith("/static/"):
if os.environ.get("RAILWAY_ENVIRONMENT"):
response.headers["Cache-Control"] = "public, max-age=31536000, immutable"
Assets estáticos com URLs de hash de conteúdo (?v=a3f8b2c1d0) ficam em cache por um ano com immutable. O hash muda quando o arquivo muda, forçando navegadores e CDNs a buscar a nova versão.
Carregamento adiado de scripts
<script defer src="{{ asset('js/vendor/alpine.min.js') }}"></script>
<script defer src="{{ asset('js/vendor/htmx.min.js') }}"></script>
<script defer src="{{ asset('js/main.js') }}"></script>
O atributo defer baixa scripts em paralelo com a análise de HTML, mas os executa após o documento ser analisado. Isso evita o bloqueio da renderização sem a complexidade do carregamento assíncrono e do gerenciamento da ordem de execução.
Otimização de imagens
As imagens usam WebP com srcset responsivo e dimensões explícitas:
OPTIMIZED_IMAGES = {
"vision-sprint": {
"webp_srcset": (
"/static/images/optimized/vision-sprint-400w.webp 400w, "
"/static/images/optimized/vision-sprint-800w.webp 800w, "
"/static/images/optimized/vision-sprint-1200w.webp 1200w"
),
"fallback": "/static/images/optimized/vision-sprint-fallback.jpg",
"width": 1200,
"height": 1045,
},
}
<picture>
<source type="image/webp"
srcset="{{ image.webp_srcset }}"
sizes="(max-width: 768px) 100vw, 50vw">
<img src="{{ image.fallback }}"
width="{{ image.width }}"
height="{{ image.height }}"
alt="{{ image.alt }}"
loading="lazy">
</picture>
Os atributos explícitos width e height evitam Cumulative Layout Shift (CLS). O atributo loading="lazy" adia imagens fora da tela. O WebP oferece arquivos de 25 a 35% menores do que JPEG com qualidade equivalente.16
Early Hints
# In main.py
app.state.preload_links = [
f'<{make_asset_url(_asset_map, "css/styles.css")}>; rel=preload; as=style',
]
# In security headers middleware
if "text/html" in content_type:
preload_links = getattr(request.app.state, "preload_links", [])
if preload_links:
response.headers["Link"] = ", ".join(preload_links)
O cabeçalho Link com rel=preload instrui Cloudflare a enviar uma resposta 103 Early Hints, permitindo que o navegador comece a buscar CSS antes de o servidor terminar de gerar a resposta de HTML.17
JavaScript mínimo
A pegada total de JavaScript:
| Biblioteca | Tamanho (minificado + compactado com gzip) |
|---|---|
| HTMX | ~16 KB |
| Alpine.js | ~15 KB |
| JS específico da página | 4-8 KB |
| Total | 35-39 KB |
Uma aplicação React típica envia de 100 a 300 KB de JavaScript do framework antes do código da aplicação.18 A abordagem sem build envia menos JavaScript porque há menos JavaScript para enviar.
Implantação
Railway
blakecrosley.com é implantado na Railway via git push:
# railway.toml
[build]
builder = "nixpacks"
[deploy]
startCommand = "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"
healthcheckPath = "/health"
healthcheckTimeout = 300
restartPolicyType = "ON_FAILURE"
restartPolicyMaxRetries = 10
O builder Nixpacks da Railway detecta o projeto Python pelo requirements.txt, instala as dependências e executa o comando de inicialização. Não é necessário um Dockerfile. O endpoint de verificação de integridade garante que a aplicação esteja respondendo antes de receber tráfego:
@app.get("/health")
async def health():
return {"status": "healthy"}
O pipeline de implantação
git push origin main
→ Railway detects push
→ Nixpacks installs Python + requirements.txt (cached)
→ uvicorn starts
→ Health check passes
→ Traffic routes to new deployment
→ ~40 seconds total
Sem npm install. Sem npm run build. Sem compilação do webpack. Sem compilação do TypeScript. A única etapa de instalação é pip install -r requirements.txt, que é armazenada em cache entre as implantações.
Procfile
web: uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
O Procfile oferece uma alternativa compatível com o Heroku. A Railway aceita tanto railway.toml quanto Procfile. A sintaxe ${PORT:-8000} usa a porta fornecida pela plataforma ou adota 8000 como padrão para o desenvolvimento local.
Configuração do Uvicorn para produção
Para implantações com tráfego mais intenso, use vários workers:
uvicorn app.main:app \
--host 0.0.0.0 \
--port ${PORT:-8000} \
--workers 4 \
--loop uvloop \
--http httptools
--workers 4executa quatro processos worker (regra geral: 2 * núcleos da CPU + 1)--loop uvloopusa o loop de eventos uvloop, que é mais rápido (substituto direto do asyncio)--http httptoolsusa o parser HTTP httptools, que é mais rápido
Cada worker é um processo separado que mantém sua própria cópia da aplicação. Por isso, a memória por processo é multiplicada pela quantidade de workers — e é exatamente aí que a correção do grafo de dependências da versão 0.140.0 do FastAPI faz diferença: em uma aplicação com muitas dependências, quatro workers na versão 0.139.2 pagam quatro vezes o antigo overhead de Dependant.27
Para desenvolvimento, --reload monitora alterações nos arquivos:
uvicorn app.main:app --reload --port 8000
Alternativa com Docker
Para plataformas que exigem Docker:
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
A imagem-base slim mantém o contêiner pequeno. --no-cache-dir impede que o pip armazene os pacotes baixados na camada da imagem.
CDN da Cloudflare
blakecrosley.com usa a Cloudflare para cache de CDN, DNS e Workers:
# Cache headers for HTML pages (set in security middleware)
response.headers["Cache-Control"] = (
"public, max-age=300, s-maxage=3600, "
"stale-while-revalidate=86400"
)
max-age=300— o navegador mantém o cache por 5 minutoss-maxage=3600— a CDN mantém o cache por 1 horastale-while-revalidate=86400— fornece conteúdo desatualizado enquanto o revalida por 24 horas
Os assets estáticos recebem max-age=31536000, immutable porque URLs com hash de conteúdo garantem que estejam sempre atualizados.
Estrutura de decisão
Você precisa de ferramentas de build?
Responda a quatro perguntas:
1. Mais de cinco desenvolvedores compartilham interfaces do JavaScript? Se sim, a verificação de tipos do TypeScript em tempo de compilação evita bugs de integração que os testes em tempo de execução detectariam tarde demais. Adicione uma etapa de build.
2. Sua aplicação gerencia estados complexos no lado do cliente? Se recursos como arrastar e soltar, colaboração em tempo real ou dados offline-first forem essenciais — e não apenas desejáveis —, a complexidade de um framework como React ou Svelte se justifica. Adicione uma etapa de build.
3. Vários produtos usam uma biblioteca de componentes compartilhada? Se sim, essa biblioteca precisa de empacotamento com npm, versionamento semântico e tree shaking. Adicione uma etapa de build.
4. Você depende de bibliotecas do ecossistema npm que pressupõem o uso de um bundler? Se Radix, Framer Motion, TanStack Query ou bibliotecas semelhantes forem essenciais para o produto, um pipeline de build será obrigatório.
Se as quatro respostas forem “não”, a abordagem sem build é viável. Se alguma resposta for “sim”, as ferramentas de build resolvem um problema real. O erro é adicioná-las quando as quatro respostas são “não” — você acaba resolvendo problemas que não tem enquanto cria um overhead de gerenciamento de dependências que antes não existia.1
Comparação das stacks
| Categoria | Sem build (este guia) | React + ferramentas de build |
|---|---|---|
| Mais indicada para | Sites de conteúdo, portfólios, ferramentas internas, aplicações CRUD | Produtos SaaS, SPAs complexas, consumidores de sistemas de design |
| Tamanho da equipe | 1-5 desenvolvedores | 5-50+ desenvolvedores |
| Gerenciamento de estado | Servidor (HTMX) + cliente (Alpine.js) | Cliente (estado do React, Redux, Zustand) |
| Segurança de tipos | Em tempo de execução (Pydantic no servidor) | Em tempo de compilação (TypeScript) |
| Reutilização de componentes | Includes + macros do Jinja2 | Pacotes npm, bibliotecas compartilhadas |
| SEO | Renderizado no servidor por padrão | Exige configuração de SSR/SSG |
| Piso de desempenho | Alto (JS mínimo, renderização no servidor) | Variável (overhead do framework) |
| Limite de complexidade | Menor (sem modo offline nem estados complexos no cliente) | Maior (qualquer interação no cliente é possível) |
| Dependências | 17 pacotes Python | Mais de 300 pacotes npm |
| Tempo de build | 0 segundos | 15-60 segundos |
Quando HTMX não é a escolha certa
HTMX substitui o estado do cliente por ciclos de ida e volta ao servidor. Isso funciona até a latência começar a importar:
- Interfaces de arrastar e soltar — um ciclo de ida e volta de 200 ms ao servidor para cada evento de arrastar é inaceitável
- Colaboração em tempo real — estados controlados por WebSocket exigem resolução de conflitos no lado do cliente
- Aplicações offline-first — sem servidor, não há HTMX
- Animações complexas vinculadas ao estado — Framer Motion e React Spring pressupõem o modelo de reconciliação do React
- Aplicações Canvas/WebGL — o loop de renderização é inerentemente executado no lado do cliente
Para esses casos de uso, um framework no lado do cliente é a ferramenta certa. A abordagem sem build não tenta substituí-los.
Cartão de referência rápida
FastAPI
# Development
source venv/bin/activate
uvicorn app.main:app --reload --port 8000
# Production
uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}
# Testing
python -m pytest -v --cov=app
# Database migrations
alembic upgrade head
alembic revision --autogenerate -m "description"
Atributos do HTMX
hx-get="/url" <!-- GET request -->
hx-post="/url" <!-- POST request -->
hx-target="#element" <!-- Where to put response -->
hx-swap="innerHTML" <!-- How to insert (innerHTML, outerHTML, beforeend) -->
hx-trigger="click" <!-- What triggers request -->
hx-trigger="keyup changed delay:300ms" <!-- Debounced input -->
hx-trigger="load" <!-- Fire on element load -->
hx-indicator="#spinner" <!-- Show during request -->
hx-push-url="true" <!-- Update browser URL -->
hx-replace-url="true" <!-- Replace URL (no history) -->
Atributos do Alpine.js
x-data="{ open: false }" <!-- Component scope + state -->
x-show="open" <!-- Toggle visibility -->
x-cloak <!-- Hide until Alpine inits -->
@click="open = !open" <!-- Event handler -->
@click.away="open = false" <!-- Outside click -->
@keydown.escape="open = false" <!-- Keyboard event -->
:class="{ 'active': open }" <!-- Dynamic class -->
:aria-expanded="open" <!-- Dynamic attribute -->
x-text="count" <!-- Dynamic text content -->
x-init="fetchData()" <!-- Run on init -->
Propriedades personalizadas do CSS
:root {
--color-bg: #000000;
--color-text: #ffffff;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
--font-size-lg: 1.25rem;
}
@media (max-width: 768px) {
:root { --gutter: 24px; }
}
Cabeçalhos de segurança
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-eval'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Cross-Origin-Opener-Policy: same-origin
Permissions-Policy: camera=(), microphone=(), geolocation=()
Checklist de configuração do projeto
[ ] FastAPI app with Jinja2Templates
[ ] Security headers middleware (CSP, HSTS, X-Frame-Options)
[ ] CSRF token generation and validation
[ ] GZip middleware (minimum_size=500)
[ ] Content-hash asset versioning (cache busting)
[ ] HTMX self-hosted in /static/js/vendor/
[ ] Alpine.js self-hosted in /static/js/vendor/
[ ] CSS custom properties for design tokens
[ ] Health check endpoint (/health)
[ ] Error handlers (404, 500)
[ ] robots.txt, sitemap.xml, llms.txt
[ ] JSON-LD structured data in base template
[ ] Hreflang tags for i18n (if multi-language)
[ ] HTML sanitization filter (nh3)
[ ] Rate limiting middleware
[ ] Deferred script loading
Perguntas frequentes
O HTMX está pronto para aplicações web reais em produção?
Sim. O HTMX está estável desde 2020 e é usado em produção em diversos setores. Carson Gross, seu criador, mantém a compatibilidade retroativa como um princípio central de design — a documentação do HTMX afirma que a biblioteca não quebrará aplicações existentes dentro de uma versão principal.19 A biblioteca tem cerca de 16 KB após minificação e compactação com gzip, não possui dependências e segue o versionamento semântico. O blakecrosley.com usa o HTMX em produção há três anos sem nenhum bug relacionado ao HTMX.20
Posso usar TypeScript sem uma etapa de build?
Parcialmente. É possível verificar os tipos dos arquivos TypeScript com tsc --noEmit sem gerar arquivos de saída, usando a verificação em tempo de compilação como um linter. Porém, os navegadores não conseguem executar arquivos .ts diretamente, portanto ainda é necessária uma etapa de build para disponibilizar TypeScript. A alternativa é usar anotações de tipo JSDoc em arquivos .js comuns, que o TypeScript consegue verificar sem compilação. Assim, você tem segurança de tipos durante o desenvolvimento enquanto entrega JavaScript padrão.
Como essa abordagem se compara ao Astro ou ao 11ty?
Astro e 11ty são geradores de sites estáticos que produzem HTML puro com o mínimo de JavaScript no cliente, mas exigem uma etapa de build (Node.js, npm install e um comando de build). A abordagem sem build elimina essa etapa — o servidor renderiza o HTML a cada requisição. A contrapartida: Astro/11ty produzem páginas estáticas mais rápidas (sem processamento no servidor), enquanto FastAPI + HTMX lidam nativamente com conteúdo dinâmico (dados específicos do usuário, envios de formulários e atualizações em tempo real) sem uma camada API separada.
E quanto à renderização no servidor (SSR) com React?
O SSR do Next.js e a abordagem FastAPI + HTMX têm o mesmo objetivo: enviar HTML renderizado pelo servidor ao navegador. A diferença está no que acontece após a renderização inicial. O Next.js hidrata a página com React, enviando o runtime do framework e o código dos componentes ao cliente. FastAPI + HTMX não fazem hidratação — o HTML é o resultado final. O HTMX cuida das interações seguintes solicitando novos fragmentos de HTML ao servidor. Resultado: FastAPI + HTMX enviam aproximadamente 35–40 KB de JavaScript no total, contra 100–300 KB em uma aplicação Next.js.18
Como faço a validação de formulários com essa stack?
No servidor. O Pydantic valida os dados quando o formulário é enviado. Se a validação falhar, o servidor retorna o formulário com mensagens de erro. O HTMX insere a resposta no DOM:
<form hx-post="/contact" hx-target="#form-container" hx-swap="outerHTML">
<input type="email" name="email" required>
<button type="submit">Send</button>
</form>
@router.post("/contact")
async def contact(request: Request, email: str = Form(...)):
if not validate_email(email):
return templates.TemplateResponse("components/_contact_form.html", {
"request": request,
"error": "Please enter a valid email address",
"email": email, # Preserve input
})
await send_email(email)
return templates.TemplateResponse("components/_contact_success.html", {
"request": request
})
O servidor valida, renderiza os estados de erro e o HTMX insere o resultado. Nenhuma biblioteca de validação no cliente é necessária. O atributo required do HTML oferece validação básica no navegador como primeira linha de defesa.
Posso adicionar recursos em tempo real (WebSockets)?
Sim. O FastAPI oferece suporte integrado a WebSocket:
from fastapi import WebSocket
@app.websocket("/ws/notifications")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await get_notification()
await websocket.send_text(render_notification_html(data))
O HTMX tem uma extensão para WebSocket (hx-ws) que conecta elementos a endpoints de WebSocket:
<!-- HTMX 2.x WebSocket extension syntax -->
<div hx-ext="ws" ws-connect="/ws/notifications">
<div id="notifications" ws-send></div>
</div>
Observação: o HTMX 1.x usava a sintaxe
hx-ws="connect:...". No HTMX 2.x, o suporte a WebSocket foi transferido para uma extensão separada (htmx-ext-ws), com os atributosws-connectews-sendmostrados acima. Se você estiver usando o HTMX 1.x, a sintaxe antigahx-wsainda funciona.Acompanhamento da versão beta do HTMX 4.0: o htmx 4.0.0-beta6 agora está disponível na tag
nextdo npm e na documentação da versão 4.0 (beta6 publicada em 23 de julho de 2026), enquanto o guia de início rápido do htmx.org e a taglatestdo npm permanecem na versão 2.0.10. Este guia ainda se concentra no HTMX 2.x, que continua sendo a versão recomendada para uso em produção até que a 4.0 fique estável; a migração da versão 2.x para a 4.x representa uma mudança de geração, não uma atualização pontual da 2.x. O padrão de versionamento da big-skies-software ignora as versões principais ímpares, portanto a 4.0 é a próxima etapa depois da 2.x.2122O que vale acompanhar na documentação da versão 4.0. Duas novidades se destacam para análise de segurança e arquitetura antes da versão final da 4.0: a nova extensão
hx-liveintroduz expressões reativas ao DOM, que são reavaliadas quando o estado referenciado muda, e a nova extensãohx-noncecondiciona o processamento dos atributos do htmx ao uso de nonces de CSP. O guia de migração da versão 4.0 também transfere vários conceitos de configuração, restaura ou altera alguns comportamentos de eventos e histórico e remove alguns helpers de JavaScript do núcleo. Trate a versão 4.0 como um projeto de migração, não como um patch da 2.x que pode ser aplicado diretamente.21
As mensagens do servidor são inseridas no DOM usando os mesmos mecanismos de destino e substituição das respostas HTTP. O servidor envia fragmentos de HTML pelo WebSocket, e o HTMX os insere.
Como essa stack lida com SEO?
O HTML renderizado pelo servidor é naturalmente adequado para SEO porque os crawlers recebem todo o conteúdo da página sem executar JavaScript. O blakecrosley.com adiciona várias camadas de SEO:
- Dados estruturados JSON-LD no
<head>de todas as páginas (schemas Person, Article, WebSite e FAQPage) - Sitemap dinâmico com alternativas hreflang para todos os 10 locais
- Feed RSS em
/blog/feed.xml llms.txtna raiz para facilitar a descoberta por crawlers de IA- URLs canônicas e tags Open Graph no template base
- HTML semântico:
<article>,<section>,<main>e hierarquia correta de títulos
Nenhuma configuração de SSR é necessária. Nenhum getStaticProps. Nenhum ISR. O HTML é renderizado a cada requisição — esse é o comportamento padrão, não uma otimização.
Como é a curva de aprendizado em comparação com React?
Para desenvolvedores de Python, a curva de aprendizado é significativamente menor. Você já conhece a linguagem. Os manipuladores de rotas do FastAPI retornam respostas de templates — o mesmo modelo mental das views do Flask ou Django. O HTMX acrescenta alguns atributos de HTML (hx-get, hx-target, hx-swap). O Alpine.js acrescenta mais alguns (x-data, x-show, @click). Não há JSX, DOM virtual, sistema de hooks, biblioteca de gerenciamento de estado nem configuração de ferramenta de build para aprender.
A documentação do HTMX cabe em uma única página longa. A documentação do Alpine.js cabe em algumas páginas. A documentação do React se estende por centenas de páginas sobre hooks, contexto, refs, efeitos, suspense, componentes de servidor e SSR com streaming.
Para desenvolvedores de JavaScript/React, a mudança é mais conceitual do que sintática. A ideia central é que o servidor controla o estado e renderiza o HTML. O gerenciamento de estado no cliente se transforma em manipulação de rotas no servidor. A busca de dados no cliente se transforma em atributos do HTMX nos elementos de HTML. A sintaxe é mais simples — o modelo mental exige abandonar a premissa das SPAs de que o cliente controla a renderização.
Registro de alterações
| Data | Alteração | Fonte |
|---|---|---|
| 2026-08-16 | Starlette 1.3.1 → 1.6.0, e uma das mudanças altera silenciosamente o próprio snippet de GZip deste guia. Mudança de comportamento: Starlette 1.5.0 (8 de agosto) expandiu DEFAULT_EXCLUDED_CONTENT_TYPES muito além de text/event-stream, passando a cobrir arquivos gzip/zip, PNG, JPEG, WebP, GIF, AVIF, audio/*, video/* e fontes WOFF/WOFF2; portanto, eles não são mais compactados por padrão. image/* foi deliberadamente mantido fora da exclusão, deixando image/svg+xml passível de compactação. Um novo exclude_content_types somente por palavra-chave substitui a lista, a correspondência não diferencia maiúsculas de minúsculas, e reatribuir a constante do módulo em runtime não tem mais efeito. O pin de runtime de FastAPI é starlette>=0.46.0, sem limite superior; portanto, uma instalação nova obtém 1.6.0, e a mudança chega ao snippet deste guia sem nenhuma ação do leitor — ambas as passagens sobre GZip foram corrigidas. A 1.5.0 também ignora respostas parciais de status 206 e faz flush por chunk transmitido; a 1.4.0 (5 de agosto) moveu chunks gzip iguais ou superiores a thread_minimum_size de 128 KiB para uma thread de trabalho, para que grandes compactações deixem de bloquear o event loop. Nova capacidade: a 1.6.0 (8 de agosto) adicionou max_body_size em Starlette/Router/Mount/Route, além de RequestBodyLimitMiddleware — uma nova subseção de Segurança o aborda, pois a limitação do corpo da requisição não existia neste guia. Também observado: encode/starlette agora redireciona para Kludex/starlette, espelhando a mudança do Uvicorn. Apenas no changelog: Uvicorn 0.52.0–0.52.3 (implementação experimental de HTTP/1.1 zttp, baseada em Zig, que o próprio release diz para não colocar diante de tráfego de produção; a recomendação do guia de usar --http httptools continua válida), Alpine.js 3.16.0/3.16.1, SQLAlchemy 2.0.52 (suporte a Python 3.15; correção de desalinhamento das colunas de resultado de synchronize_session="fetch" em ORM UPDATE). Verificados sem alteração: FastAPI 0.141.1, HTMX 2.0.10 mais recente / 4.0.0-beta6 próxima, Bootstrap 5.3.8, Jinja2 3.1.6, Pydantic 2.13.4 estável. Nenhum novo alerta de segurança nas nove dependências durante a janela. |
28 |
| 2026-07-29 | FastAPI 0.141.0 + 0.141.1 (ambos em 29 de julho). A 0.141.0 adiciona app.frontend(check_dir="auto"), então fastapi dev não falha mais quando o diretório de build está ausente — o caso normal de iniciar o servidor antes da execução do build do frontend. A 0.141.1, horas depois, corrige dependências em app.frontend() que descartavam tarefas em segundo plano e cabeçalhos de resposta; uma dependência que definia um cookie ou agendava uma BackgroundTask tinha esse trabalho descartado no mount do frontend, embora funcionasse corretamente em rotas API, fechando uma lacuna real no suporte a dependências adicionado na 0.139.0. Ambas entram na narrativa existente de app.frontend(), e não em uma nova seção, pois a abordagem renderizada no servidor deste guia não monta um diretório dist/. A 0.141.1 também documenta FASTAPI_ENV no guia CLI de FastAPI (apenas documentação, sem mudança no corpo). |
29 |
| 2026-07-27 | FastAPI lançou 0.140.1 até 0.140.7 em cinco horas e meia no dia 27 de julho — sete releases, todos refactors da infraestrutura de dependências iniciada na 0.140.0. Há duas linhas: a cópia achatada do grafo de dependências que FastAPI construía e mantinha foi removida (0.140.2), junto a todos os locais restantes que reconstruíam uma — geração de OpenAPI (0.140.3, 0.140.7), campos de corpo (0.140.5), parâmetros de requisição (0.140.6) — e a 0.140.4 remove a contabilidade não lida de rastreamento de repetições. A única mudança com um limite visível é a 0.140.1: o lru_cache dos helpers de classificação de callable em fastapi/dependencies/models.py passa de 1.024 para 4.096 entradas (chamado _CALLABLE_CLASSIFICATION_CACHE_SIZE), após relatos de apps com mais de 1.024 dependências distintas causando thrashing no cache. Sem mudanças em API; a recomendação no parágrafo sobre memória de dependências passa de 0.140.0 para 0.140.7 ou posterior, com uma nota de que a linha ainda está em movimento e de que os benchmarks de dependências do OpenAPI (PR #16075) só chegaram no último release da série. |
30 |
| 2026-07-25 | FastAPI 0.140.0 (24 de julho, 21:16 UTC) corrige uma regressão de memória do sistema de dependências que estava presente desde a 0.121.0 (3 de novembro de 2025). A PR #16049 remove dez atributos functools.cached_property de Dependant, transfere-os para helpers no nível do módulo e transforma a classe em um @dataclass(slots=True); a execução oficial do CodSpeed na PR mesclada informa o benchmark de memória test_dependency_graph em 17,5 MB → 1,1 MB (×16), e o relato original descrevia OOM em produção na 0.121.3, enquanto a 0.120.4 se mantinha abaixo de ~400 MB. Adicionada uma passagem sobre a 0.140.0 em Padrões Async e uma linha sobre memória de workers em Configuração de produção do Uvicorn. Também foi corrigido um erro preexistente: o guia afirmava que a 0.137.0 “fixa Starlette na linha 1.x” — não fixa. O requisito de runtime de FastAPI é starlette>=0.46.0 (um mínimo, sem limite superior, ainda satisfeito por Starlette 0.4x) de maneira uniforme em 0.136.3, 0.137.0, 0.138.0, 0.139.2 e 0.140.0; os números 1.x nas notas da 0.137.0 são atualizações do dependabot para o lockfile de testes do repositório (a PR #15722 altera apenas uv.lock). A afirmação no corpo e 24 foram corrigidas. Duas não alterações foram sinalizadas: os internos de Dependant agora são incompatíveis com ferramentas (oauth_scopes, cache_key, _uses_scopes, _is_security_scheme deixaram de existir como atributos, substituídos pelas funções de módulo _get_oauth_scopes() / _get_cache_key() / _uses_scopes(), e slots=True bloqueia monkey-patching de instâncias) — API interno e não documentado, que este guia nunca referencia, na mesma categoria da mudança de router.routes na 0.137.0; e a documentação oficial de FastAPI agora prioriza projetos uv em vez de pip/venv em 30 arquivos, incluindo README, index.md, virtual-environments.md e as páginas de Docker/deploy (PR #16032, mesclada em 21 de julho). A mudança na documentação é cosmética para seu código, mas o guia ensina pip install -r requirements.txt em todo o material e agora diverge da porta de entrada do upstream — uma futura decisão editorial, deliberadamente não tomada nesta revisão. |
27 |
| 2026-07-24 | htmx 4.0.0-beta6 substitui beta5 como a tag npm next (publicada em 23 de julho de 2026; release de GitHub no mesmo dia). Itens principais da beta: nova extensão hx-multipart (respostas transmitidas multipart/mixed/multipart/parallel com cabeçalhos de ação HX-* por parte), restauração da rolagem do histórico pela Navigation API com fallback para Firefox, renomeação de evento interno da beta htmx:swap:finally → htmx:finally:swap, eventos do cabeçalho de resposta HX-Trigger agora são disparados após o swap, métodos de requisição personalizados e uma reescrita de hx-ws com encaminhamento de protocols. A recomendação não mudou — produção permanece em HTMX 2.x (latest = 2.0.10) até o GA da 4.0; a renomeação é incompatível apenas dentro da linha beta 4.0. Nota sobre a trilha beta e 21 atualizadas. FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1, Jinja2 3.1.6 foram todos verificados sem alteração; nenhum alerta de segurança na janela. |
|
| 2026-07-17 | FastAPI 0.139.1 + 0.139.2 (16 de julho): correção de caminho com ponto para fallbacks de app.frontend() (/users/john.doe, PR #16011) e construção thread-safe de rotas do router para testes em threads paralelas (PR #16013) — sem mudança de API voltada ao app. Uvicorn 0.49.0 → 0.51.0: a implementação legada de websockets foi descontinuada e auto agora assume como padrão websockets-sansio (0.50.0), a implementação padrão exige websockets>=13.0 (0.50.2), e a 0.51.0 (8 de julho) adiciona reinicializações de workers com SIGHUP e sobreposição para reloads com quase nenhuma indisponibilidade; o repositório agora está em Kludex/uvicorn. HTMX (2.0.10 / 4.0.0-beta5 next), Alpine.js 3.15.12, Starlette 1.3.1, Pydantic 2.13.4, SQLAlchemy 2.0.51, Bootstrap 5.3.8 foram todos verificados sem alteração; nenhum alerta de segurança na janela. |
|
| 2026-07-07 | htmx 4.0.0-beta5 agora é a tag npm next (publicada em 26 de junho de 2026), substituindo beta4; a nota da trilha beta de HTMX 4.0 e [^22] foram atualizadas para corresponder. A recomendação não mudou — o trabalho de produção permanece em HTMX 2.x (latest = 2.0.10) até o GA da 4.0. Verificado em relação às dist-tags npm de htmx.org. |
|
| 2026-07-02 | FastAPI 0.139.0 (1º de julho). app.frontend() agora oferece suporte a dependências — por exemplo, autenticação automática por cookie para o frontend servido (PR #15908) — estendendo o mount de frontend estático da 0.138.0 com a infraestrutura padrão de Depends(); continua ortogonal à tese de renderização no servidor deste guia, observada no mesmo parágrafo de contraste. Nenhuma outra movimentação na stack: HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8, SQLAlchemy 2.0.51 permanecem inalterados. |
26 |
| 2026-06-22 | FastAPI 0.138.0 + 0.137.2. A 0.138.0 (20 de junho) adiciona app.frontend("/", directory="dist") / router.frontend(...) para servir um frontend estático compilado (saída dist/ de SPA) — ortogonal à tese deste guia de renderização no servidor sem build, observada como contraste na seção Padrões Async. A 0.137.2 (18 de junho) adiciona iter_route_contexts() como a forma suportada de enumerar rotas agora que router.routes é interno (desde a 0.137.0). Ambas são adições de recursos, sem mudanças incompatíveis; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8), SQLAlchemy (2.0.51) permanecem inalterados. |
25 |
| 2026-06-16 | FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. A 0.137.0 de FastAPI (14 de junho) refatora os internos do router: router.routes agora é uma árvore interna, não uma lista plana de APIRoute (incompatível para qualquer coisa que a percorra), ao mesmo tempo que habilita rotas adicionadas após include_router() e novos hooks APIRouter.matches()/.handle(); a 0.137.1 (15 de junho) corrige a tipagem de APIRoute e routers sem prefixo com caminho vazio. Starlette lançou sua primeira 1.0 estável (22 de março) e agora está na 1.3.1 (12 de junho), removendo os hooks descontinuados on_event/on_startup/on_shutdown e os decorators @app.route()/@app.websocket_route() — lifespan e Route/WebSocketRoute são os únicos caminhos. (Esta entrada afirmava originalmente que FastAPI 0.137.0 fixa Starlette 1.3.1 — corrigido em 2026-07-25: não fixa; o requisito de runtime é starlette>=0.46.0, sem limite superior.) Foi adicionada uma nota sobre lifespan/router à seção Padrões Async. SQLAlchemy 2.0.51 (15 de junho) contém apenas correções de bugs. |
24 |
| 2026-06-08 | Mudança de instalação async no SQLAlchemy 2.0.50. A partir do SQLAlchemy 2.0.50, a dependência greenlet da stack async deixou de ser instalada por padrão — instale o extra sqlalchemy[asyncio] (caso contrário, o primeiro await no engine falha com um erro de greenlet ausente). A 2.0.50 também exige Python 3.10+ (3.7–3.9 foram removidos) e adiciona wheels para 3.13t com free-threading. Foi adicionada uma nota de instalação à seção Async do SQLAlchemy 2.0. Sem mudanças no corpo para o restante da stack: a versão mais recente de FastAPI continua 0.136.3 (2026-05-23, sem release em junho), htmx estável continua 2.0.10 (4.0.0-beta4 “The Fetchening” está em beta com uma meta de estável para ~início de 2027, ainda não sendo uma recomendação para produção), Alpine.js 3.15.12, Bootstrap 5.3.x inalterado. A recomendação para produção não mudou: HTMX 2.x até a estabilidade da 4.0.23 |
|
| 2026-05-24 | Verificação de manutenção: o inventário de conteúdo local ainda mostra 210 posts de blog, 11 guias principais, 48 estudos de design e 10 locais compatíveis, incluindo inglês. A versão mais recente de FastAPI é 0.136.3 (2026-05-23); o único refactor voltado ao app mencionado nas notas de release é o tratamento mais rigoroso de cabeçalhos com sublinhado quando convert_underscores=True, e a 0.136.2 valida campos de Server-Sent Event para evitar dados de evento quebrados. htmx estável continua 2.0.10, enquanto npm next e a documentação 4.0 agora apontam para 4.0.0-beta4; a versão mais recente do SQLAlchemy 2.0 é 2.0.50; a versão mais recente do Pydantic continua 2.13.4. A recomendação para produção permanece inalterada: use HTMX 2.x até a estabilidade da 4.0.122 |
|
| 2026-05-18 | Atualização do inventário do site: o inventário de conteúdo local agora mostra 210 posts de blog, 11 guias principais, 48 estudos de design e 10 locais compatíveis, incluindo inglês. A versão mais recente de FastAPI continua 0.136.1; htmx estável continua 2.0.10 com npm next em 4.0.0-beta3; a versão mais recente npm de Alpine.js continua 3.15.12. A recomendação para produção permanece inalterada: use HTMX 2.x até a estabilidade da 4.0.12021 |
|
| 2026-05-15 | Verificação de manutenção: a versão mais recente de FastAPI continua 0.136.1; este ambiente local do site importa FastAPI 0.128.0 e Starlette 0.50.0; htmx estável continua 2.0.10 e npm next agora é 4.0.0-beta3; a versão mais recente npm de Alpine.js é 3.15.12; a versão mais recente do Bootstrap é 5.3.8; a versão mais recente do SQLAlchemy 2.0 é 2.0.49; a versão mais recente do Pydantic é 2.13.4. A recomendação para produção não mudou: use HTMX 2.x até a estabilidade da 4.0.2021 |
|
| 2026-05-09 | Acompanhamento de htmx 4.0.0-beta3 (8 de maio de 2026): htmx 4.0.0-beta3 está disponível na tag npm next e na documentação 4.0, enquanto npm latest continua 2.0.10. Destaques que vale acompanhar antes do GA: nova extensão hx-live (expressões reativas ao DOM), nova extensão hx-nonce (proteção de nonce CSP para atributos htmx) e mudanças no guia de migração relacionadas à configuração, histórico, eventos e helpers principais de JavaScript. Recomendação para produção inalterada: htmx 2.x continua sendo a tag npm mais recente e a versão recomendada até o GA da 4.0.21 |
|
| 2026-05-07 | Verificação de manutenção: a versão mais recente de FastAPI continua 0.136.1; htmx estável é 2.0.10 e v4 continua em beta com uma meta para o verão de 2026; a versão mais recente npm de Alpine.js é 3.15.12; a versão mais recente do Bootstrap é 5.3.8; a versão mais recente do SQLAlchemy 2.0 é 2.0.49; a versão mais recente do Pydantic é 2.13.4. As métricas locais do site foram atualizadas para 182 posts de blog, 11 guias, dez locais compatíveis e 17 requisitos de Python. A orientação de migração não mudou: use HTMX 2.x em produção até a estabilidade da 4.0.20 | |
| 2026-04-25 | FastAPI 0.136.1 (23 de abril de 2026): limpeza de deprecações do Pydantic v2 (sem mudanças comportamentais para o código do app). Cronograma de HTMX 4.0 acompanhado: htmx 4.0.0-beta1 (6 de abril) e 4.0.0-beta2 (14 de abril) foram lançados. Orientação de migração inalterada — htmx 2.x permanece na tag npm mais recente até a estabilidade da 4.0; correções de segurança continuam, sem pressão para upgrade. Principais mudanças da 4.0 que já vale considerar no design: (1) fetch() substitui XMLHttpRequest como infraestrutura ajax principal, (2) a herança de atributos torna-se explícita por padrão, (3) o suporte a histórico emite uma requisição de rede para conteúdo restaurado (sem snapshot local do DOM). FastAPI 0.135.4 (16 de abril) removeu o decorator de 1º de abril @app.vibe() que chegou na 0.135.3. |
|
| 2026-04-16 | Adicionada a conscientização sobre HTMX 4.0-beta (referência futura). Registrado o suporte de FastAPI 0.136.0 a builds de Python 3.14t com free-threading. Recursos do Pydantic 2.13.x (fábricas padrão de atributos privados com acesso a dados validados do modelo, namespace pydantic.v1 para 1.10.26 com suporte a 3.14). Correções de Alpine.js 3.15.11: modificador x-anchor.noflip, aviso de múltiplos elementos raiz em x-for, correção de regressão no morph de $refs. |
|
| 2026-03-24 | Publicação inicial |
Referências
Este guia aborda o sistema completo usado para criar o blakecrosley.com. O No-Build Manifesto apresenta o argumento filosófico. O post Lighthouse Perfect Score documenta a jornada de otimização de performance. O post Vibe Coding vs. Engineering explora onde o desenvolvimento assistido por IA se encaixa neste fluxo de trabalho.
-
Métricas de produção do blakecrosley.com em 18 de maio de 2026. O site tem 210 posts de blog, componentes interativos JavaScript, 11 guias principais, 48 estudos de design, inglês mais 9 locais traduzidos, dependências Python mínimas e nenhuma ferramenta de build. Verificado com base no inventário local de conteúdo, em
app/i18n/config.pye emrequirements.txt. ↩↩↩↩↩ -
O Google PageSpeed Insights (pagespeed.web.dev) executa auditorias do Lighthouse em qualquer URL pública. O blakecrosley.com obtém 100/100/100/100 (Performance, Accessibility, Best Practices, SEO) desde março de 2026. Os resultados podem ser verificados publicamente. Consulte De 76 a 100: alcançando uma pontuação perfeita no Lighthouse para acompanhar toda a jornada de otimização. ↩↩↩
-
Um
npx create-next-app@latestnovo (Next.js 15, testado em fevereiro de 2026) instala 311 pacotes emnode_modules/, totalizando 187 MB. Projetos de produção com dependências adicionais tendem a ficar maiores. Cada projeto varia. Fonte: testes do autor, documentados em The No-Build Manifesto. ↩ -
A documentação de performance do Next.js da Vercel recomenda otimizações específicas (otimização de imagens, carregamento de fontes e code splitting) para atingir pontuações acima de 90. Consulte nextjs.org/docs/app/building-your-application/optimizing. A faixa de 70 a 90 reflete as configurações padrão antes da aplicação dessas otimizações. ↩↩
-
Lista completa de dependências verificada em
requirements.txtdo blakecrosley.com em maio de 2026. O arquivo atualmente tem 17 entradas de requisitos Python e nenhuma ferramenta de build, compilador ou bundler. ↩ -
Com base na experiência do autor mantendo projetos Next.js (2021–2024), o ecossistema JavaScript gera de 15 a 25 PRs do Dependabot por mês em projetos ativos, e a maioria atualiza dependências transitivas que o desenvolvedor nunca importou diretamente. ↩
-
Tim Berners-Lee formulou a compatibilidade retroativa como um princípio de design da web: “um navegador deve ser compatível com versões anteriores”. Uma página de 1996 é renderizada no Chrome de 2026. Consulte w3.org/DesignIssues/Principles. ↩
-
A OWASP recomenda desabilitar endpoints de documentação API em produção para reduzir a superfície de ataque. O endpoint
/openapi.jsonexpõe todas as definições de rota, parâmetros e modelos de resposta. ↩ -
Documentação do FastAPI sobre handlers async versus sync: fastapi.tiangolo.com/async/. Misturar
awaitcom chamadas bloqueantes em funçõesasyncdeixa o loop de eventos sem recursos. ↩ -
nh3 é um sanitizador HTML baseado em Rust, sucessor da biblioteca Bleach. Ele é mantido pelo projeto PyO3 e oferece sanitização HTML baseada em allowlist. Consulte github.com/messense/nh3. ↩
-
O cabeçalho
Varyé definido na seção 12.5.5 da RFC 9110. Ele instrui os caches a armazenar respostas separadas com base nos valores especificados dos cabeçalhos de requisição. SemVary: HX-Request, uma CDN poderia servir um fragmento HTMX como uma resposta de página completa. Consulte httpwg.org/specs/rfc9110.html#field.vary. ↩↩ -
As Custom Properties do CSS (variáveis do CSS) são compatíveis com mais de 97% dos navegadores globais. Elas fazem cascade, são herdadas e respondem a media queries em runtime — capacidades que as variáveis de preprocessadores não têm. Fonte: caniuse.com/css-variables. ↩
-
Documentação do Google sobre hreflang: developers.google.com/search/docs/specialty/international/localized-versions. O valor
x-defaultdesigna a página de fallback para usuários cujo idioma não está na lista de hreflang. ↩ -
Alpine.js exige
'unsafe-eval'na Content Security Policy para seu mecanismo de avaliação de expressões. O build compatível com CSP (@alpinejs/csp) evita essa exigência, mas tem limitações. Consulte alpinejs.dev/advanced/csp. ↩ -
Tokens CSRF baseados em HMAC seguem o padrão “Signed Double-Submit Cookie”, descrito na OWASP CSRF Prevention Cheat Sheet.
hmac.compare_digestusa comparação em tempo constante para evitar ataques de canal lateral por timing. Consulte cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. ↩ -
WebP fornece arquivos 25–35% menores que JPEG com qualidade visual equivalente. Estudo do Google sobre WebP: developers.google.com/speed/webp/docs/webp_study. ↩
-
103 Early Hints permite que o servidor (ou a CDN) envie uma resposta preliminar com dicas de preload antes de a resposta final estar pronta. Cloudflare oferece suporte a Early Hints para cabeçalhos
Linkcomrel=preload. Consulte developer.chrome.com/blog/early-hints. ↩ -
React 18 + ReactDOM pesam aproximadamente 42 KB minificados + compactados com gzip. Com um router, uma biblioteca de gerenciamento de estado e o runtime de um framework de build, aplicações React típicas enviam de 100 a 300 KB de JavaScript do framework. Fonte: bundlephobia.com/package/react-dom@18.2.0. ↩↩
-
A política de versionamento e o compromisso com compatibilidade retroativa do HTMX estão documentados em htmx.org/migration-guide-htmx-1/. Carson Gross declarou o princípio de compatibilidade retroativa em Hypermedia Systems (2023), de Gross, Stepinski e Cotter: hypermedia.systems. ↩
-
Verificação de manutenção de 15 de maio de 2026. O PyPI e as notas de versão do FastAPI listam a versão 0.136.1; a verificação de importação local retornou FastAPI 0.128.0 e Starlette 0.50.0 para o ambiente deste site; htmx.org lista 2.0.10 no quick start;
npm view htmx.org version dist-tagsretornoulatest=2.0.10enext=4.0.0-beta3;npm view alpinejs versionenpm view @alpinejs/csp versionretornaram3.15.12; o blog oficial do Bootstrap e os metadados do pacote npm listam 5.3.8; o PyPI e a documentação do SQLAlchemy listam 2.0.49; o PyPI do Pydantic lista 2.13.4. ↩↩↩↩ -
htmx 4.0.0-beta6 é a tag npm
nextatual (publicada em 23 de julho de 2026; a linha beta passou de beta3 em 8 de maio de 2026 → beta4 → beta5 → beta6), enquanto a npmlatestcontinua sendo 2.0.10. A documentação 4.0 em four.htmx.org acompanha o buildnext, o índice de extensões 4.0 listahx-liveehx-nonce, e o guia de migração 4.0 documenta alterações de migração que você deve revisar antes de migrar aplicações de produção da versão 2.x. Verificado nas dist-tags npm dohtmx.orgem 24 de julho de 2026. ↩↩↩↩↩↩ -
Verificação de manutenção de 24 de maio de 2026. Comandos de inventário local retornaram 210 posts de blog em Markdown, 11 arquivos de guia de nível superior e 48 arquivos de estudos de design. As notas de versão do FastAPI listam 0.136.3 em 2026-05-23, com tratamento mais rigoroso de cabeçalhos com sublinhado quando
convert_underscores=True; 0.136.2 valida campos de Server-Sent Event.python3 -m pip index versions fastapiretornou a versão mais recente0.136.3;python3 -m pip index versions sqlalchemyretornou a versão mais recente2.0.50;python3 -m pip index versions pydanticretornou a versão mais recente2.13.4.npm view htmx.org dist-tags version time.modified --jsonretornoulatest=2.0.10,next=4.0.0-beta4etime.modified=2026-05-22T15:56:21.948Z; as docs de instalação do four.htmx.org mostramhtmx.org@4.0.0-beta4. ↩↩ -
Changelog do SQLAlchemy 2.0.50 e blog de lançamento, lançados em 2026-05-24. A dependência asyncio
greenletnão é mais instalada por padrão; o destino de instalaçãosqlalchemy[asyncio]agora é necessário para incluí-la. A versão 2.0.50 também deixa de oferecer suporte ao Python 3.7/3.8/3.9 (agora 3.10+), adiciona wheels Python com free-threading e adiciona um parâmetro de window frameover(..., exclude=...). Última versão verificada no PyPI em 2026-06-08. htmx 4.0.0-beta4 (“The Fetchening”, 2026-05-22) continua beta, com a versão estável prevista para o início de 2027; FastAPI 0.136.3 (2026-05-23), Alpine.js 3.15.12 e Bootstrap 5.3.x permanecem inalterados neste período. ↩↩↩ -
Notas de versão do FastAPI release notes: 0.137.0 (2026-06-14) refatora componentes internos do router para que
router.routesdeixe de ser uma lista plana de objetosAPIRoutee passe a ser uma árvore de objetos intermediários (trate como interno); também permite adicionar rotas depois deinclude_router(), incluindo um sub-router antes de suas rotas serem definidas, evita copiar rotas e adicionaAPIRouter.matches()/.handle(). Ele não fixa o Starlette na versão 1.x: o requisito de runtime do FastAPI éstarlette>=0.46.0— um limite mínimo sem limite máximo — de forma idêntica em 0.136.3, 0.137.0, 0.138.0, 0.139.2 e 0.140.0, verificado nos metadadosrequires_distdo JSON JSON API no PyPI em 2026-07-25. A linha “bump starlette from 1.1.0 to 1.2.1” (PR #15722) nas notas da 0.137.0 é uma atualização do dependabot em Internal que afeta apenas o lockfile de testeuv.lockdo repositório. (Um limite máximo existiu anteriormente — 0.120.4 e 0.121.0 incluíamstarlette<0.50.0,>=0.40.0— mas foi removido na 0.136.3.) Correção aplicada em 2026-07-25; a redação anterior desta nota de rodapé e a afirmação no corpo estavam erradas. A versão 0.137.1 (2026-06-15) corrige a tipagem deAPIRoutee um caminho vazio em um router sem prefixo. Notas de versão do Starlette release notes: 1.0.0 (2026-03-22), sua primeira versão estável em cerca de 8 anos, removeuon_startup/on_shutdown/on_event()e os decorators@app.route()/@app.websocket_route()(uselifespaneRoute/WebSocketRoute); a mais recente é 1.3.1 (2026-06-12). SQLAlchemy 2.0.51 (changelog, 2026-06-15) contém apenas correções de bugs, sem impacto em async ou na instalação. Verificado via PyPI e pelas notas de versão oficiais em 2026-06-16. ↩↩↩ -
Notas de versão do FastAPI release notes: 0.138.0 (2026-06-20) adiciona
app.frontend("/", directory="dist")erouter.frontend("/", directory="dist")para servir um frontend estático compilado (PR #15800; docs de Frontend) — um recurso estático para servir SPA dedist/, não um padrão de renderização no servidor; sem breaking change. 0.137.2 (2026-06-18) adicionaiter_route_contexts()para uso avançado que antes percorriarouter.routes(interno desde 0.137.0); sem breaking change. Nenhum lançamento mais recente que 0.138.0 até 2026-06-22. Starlette 1.3.1, Pydantic 2.13.4, Uvicorn 0.49.0, SQLAlchemy 2.0.51, HTMX 2.0.10, Alpine.js 3.15.12 e Bootstrap 5.3.8 permanecem inalterados. Verificado via PyPI e pelas notas de versão oficiais em 2026-06-22. ↩↩ -
Notas de versão do FastAPI 0.139.0, 1º de julho de 2026: “Support dependencies in
app.frontend(), e.g. for automatic cookie authentication for the frontend” (PR #15908). O restante da versão são traduções, documentação e atualizações de dependências; sem breaking changes. Verificação da sessão atual em 2 de julho de 2026 (PST): 0.139.0 é a versão mais recente na página de releases do GitHub. ↩↩ -
Notas de versão do FastAPI 0.140.0, publicadas em 24 de julho de 2026 às 21:16 UTC (PyPI
upload_time_iso_86012026-07-24T21:16:42Z). A única entrada de refatoração é “⚡️ Reduce memory usage in dependencies. PR #16049” (mesclada em 2026-07-24T21:07:52Z). A regressão foi introduzida pela PR #14262 (mesclada em 2025-11-03), lançada na 0.121.0 no mesmo dia, que adicionoufunctools.cached_propertyaDependant.cache_key; na 0.139.2, a classe tinha dez definições de@cached_property. Na 0.140.0,fastapi/dependencies/models.pydeclara@dataclass(slots=True) class Dependant, com a lógica movida para_get_cache_key(),_get_oauth_scopes(),_uses_scopes()e_is_security_scheme()no nível do módulo — fonte verificada na tag 0.140.0. O bot CodSpeed na PR mesclada informa o benchmark de memóriatest_dependency_graphem 17,5 MB (base) → 1,1 MB (head), “improve performance by ×16”; a 0.140.0 também adiciona um benchmark de memória no CI (PR #16046) para evitar uma nova regressão. O relato original está na discussion #14742, na qual a 0.120.4 permanecia abaixo de aproximadamente 400 MB e a 0.121.3 apresentava OOM em produção. Observação para autores de tooling:Dependant.oauth_scopes,.cache_key,._uses_scopese._is_security_schemenão existem mais como atributos, eslots=Trueimpede monkey-patching de instâncias — uma API interna não documentada que este guia não usa, na mesma categoria da alteração derouter.routesda 0.137.0. Todos os fatos foram verificados novamente no PyPI, no API do GitHub e no código-fonte da tag em 2026-07-25. ↩↩↩ -
Releases do Starlette 1.4.0 (2026-08-05), 1.5.0 (2026-08-08) e 1.6.0 (2026-08-08). A 1.5.0 tem o título “This release is all about giving
GZipMiddlewaresome love” e lista “Addexclude_content_typesparameter toGZipMiddleware”, “Flush GZip output for each streamed chunk”, “Skip compression of partial responses inGZipMiddleware” e “Expand default excluded content types inGZipMiddleware”. A tupla de exclusão e a assinatura foram lidas diretamente de starlette/middleware/gzip.py:DEFAULT_EXCLUDED_CONTENT_TYPES= application/gzip, application/x-gzip, application/zip, audio/, font/woff, font/woff2, image/avif, image/gif, image/jpeg, image/png, image/webp, text/event-stream, video/ — edef __init__(self, app, minimum_size=500, compresslevel=9, thread_minimum_size=128*1024, *, exclude_content_types=DEFAULT_EXCLUDED_CONTENT_TYPES). A 1.6.0 adicionamax_body_sizeem Starlette/Router/Mount/Route, além deRequestBodyLimitMiddleware. Tudo foi obtido e verificado em 2026-08-16. ↩↩↩↩ -
FastAPI 0.141.0 (2026-07-29, 14:47 UTC) adicionou
app.frontend(check_dir="auto")para desenvolvimento local comfastapi dev(PR #16102). FastAPI 0.141.1 (2026-07-29, 17:17 UTC) corrigiu o suporte a tarefas em segundo plano e cabeçalhos de dependências emapp.frontend()(PR #16105) e documentouFASTAPI_ENVno guia de CLI do FastAPI (PR #16104). Ambas por @tiangolo. PyPI confirmou a 0.141.1 como mais recente em 2026-07-29. ↩↩ -
Releases do FastAPI de 0.140.1 até 0.140.7, todas publicadas em 2026-07-27 entre 12:07 e 17:34 UTC (PyPI
upload_time_iso_8601: 0.140.112:07:51Z, 0.140.214:15:38Z, 0.140.315:30:52Z, 0.140.415:46:49Z, 0.140.516:02:53Z, 0.140.616:31:48Z, 0.140.717:34:47Z). O corpo de cada release contém uma única entrada de Refactors: 0.140.1 “Update the lru_cache limit for dependencies to account for large apps” (PR #16062); 0.140.2 “Stop retaining flat dependency trees” (PR #16065); 0.140.3 “Avoid repeated dependency flattening in OpenAPI” (PR #16067); 0.140.4 “Skip unused dependency repeat bookkeeping” (PR #16069); 0.140.5 “Avoid flattening dependencies for body fields” (PR #16071); 0.140.6 “Avoid flattening dependencies for request parameters, mainly for OpenAPI” (PR #16073); 0.140.7 “Avoid flattening dependencies for OpenAPI” (PR #16076). O número do cache vem do diff #16062, que substitui três decorators@lru_cache(maxsize=1024)emfastapi/dependencies/models.pypor@lru_cache(maxsize=_CALLABLE_CLASSIFICATION_CACHE_SIZE)e atualizatests/test_dependency_models.pypara verificarcache_info.maxsize == 4096; o corpo da PR afirma: “Some users reported a number of dependencies larger than 1024, this should account for larger apps.” A 0.140.2 também adiciona um benchmark de memória (PR #16064) e a 0.140.7 adiciona benchmarks de dependência OpenAPI (PR #16075), portanto a cobertura de benchmarks é posterior à maior parte da série. Verificado no API de releases do GitHub, nos diffs das PRs e no PyPI em 2026-07-27; 0.140.7 era a versão mais recente no momento da redação. ↩↩