FastAPI + HTMX: full-stack sem processo de build
# Crie aplicações web prontas para produção sem React ou webpack: FastAPI, HTMX, Alpine.js, Jinja2, CSS puro, padrões do 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 middlewares:
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 são importantes aqui. Primeiro, docs_url=None e openapi_url=None desativam os endpoints automáticos de documentação da API. Um site público de conteúdo não precisa expor /docs ou /openapi.json na internet.8 Segundo, a ordem dos middlewares é importante — o registro de segurança é executado primeiro (adicionado por último), portanto captura todas as solicitações, inclusive as rejeitadas pelo rate limiting. Terceiro, GZipMiddleware compacta todas as respostas com mais de 500 bytes, o que normalmente reduz o tamanho da transferência de HTML em 70–80%.
Roteamento
As rotas são divididas em duas categorias: as rotas de página retornam documentos HTML completos, enquanto as rotas de 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,
})
Essa distinção é importante para a HTMX. As rotas de página completa retornam documentos que estendem base.html. As rotas de API retornam fragmentos de HTML que a HTMX insere em elementos existentes do DOM. O mesmo mecanismo de templates Jinja2 renderiza ambos — não há uma camada de API separada.
Injeção de dependências
O sistema Depends() da FastAPI oferece uma separação clara entre os handlers das rotas 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 podem ser compostas. Uma dependência get_db pode depender de get_current_locale, que depende da solicitação. A FastAPI resolve a cadeia automaticamente.
Configurações do Pydantic
A configuração usa BaseSettings do Pydantic, com precedência para 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 os valores padrão. A classe Settings valida os tipos durante a inicialização — um campo obrigatório ausente provoca uma falha imediata, em vez de apenas durante a execução.
Padrões async
As rotas da FastAPI são async por padrão. Em operações limitadas por I/O (consultas ao banco de dados, solicitações HTTP e leituras de arquivos), async evita o bloqueio do event loop:
@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 para inicialização e encerramento. Starlette chegou à sua primeira versão estável, a 1.0, em março de 2026 (1.3.1 em 12 de junho) e removeu os hooks on_event, on_startup e on_shutdown, há muito tempo descontinuados — lifespan (acima) é o único mecanismo, e @app.route() / @app.websocket_route() deram lugar a Route / WebSocketRoute na lista routes. A FastAPI 0.137.0 (14 de junho de 2026) reformula a parte interna do próprio roteador: router.routes deixou de ser uma lista simples de objetos APIRoute e passou a ser uma árvore de nós intermediários, portanto trate-a como um detalhe interno, não como algo a ser percorrido. A vantagem é que as 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 que suas rotas sejam definidas. A própria FastAPI não fixa o Starlette na linha 1.x: seu requisito de runtime é apenas um limite mínimo, starlette>=0.46.0, desde a versão 0.136.3 e permanece inalterado até a 0.140.7 — sem limite máximo, e o Starlette 0.4x ainda atende ao requisito. Os números de versão 1.x nas notas da versão 0.137.0 são atualizações do dependabot para o arquivo de lock de testes do próprio repositório, não uma restrição de runtime para a sua aplicação.24 Nada disso altera os padrões deste guia — ele usa lifespan e a declaração padrão de rotas em toda parte —, mas, se você mantém ferramentas que percorrem router.routes ou ainda executa handlers antigos de @app.on_event, a versão 0.137.0 / Starlette 1.0 contém mudanças incompatíveis. A FastAPI 0.137.2 (18 de junho de 2026) complementa a mudança com iter_route_contexts(), a forma compatível de enumerar rotas agora que router.routes é interno. Em seguida, a FastAPI 0.138.0 (20 de junho de 2026) adiciona app.frontend("/", directory="dist") / router.frontend(...) para servir um frontend estático compilado — útil se você distribui uma compilação de SPA separada, mas sem relação direta com a abordagem deste guia, que não exige compilação e é renderizada no servidor (ela monta uma pasta dist/, em vez de renderizar HTML no servidor).25 A FastAPI 0.139.0 (1º de julho de 2026) amplia esse recurso com suporte a dependências em app.frontend() — por exemplo, autenticação automática por cookie para o frontend servido —, levando ao ponto de montagem do frontend estático o mesmo mecanismo Depends() usado nas rotas de API.26 A FastAPI 0.141.0 (29 de julho de 2026) adiciona app.frontend(check_dir="auto"), que impede que fastapi dev falhe quando a pasta da compilação ainda não existe — o caso comum em que você inicia o servidor antes de executar a compilação do frontend. A FastAPI 0.141.1, lançada no mesmo dia, corrige o descarte silencioso de tarefas em segundo plano e cabeçalhos de resposta por dependências em app.frontend(): o trabalho de uma dependência que definia um cookie ou agendava uma BackgroundTask era descartado no ponto de montagem do frontend, embora funcionasse normalmente nas rotas de API. Se você adotou o suporte a dependências da versão 0.139.0, a 0.141.1 é a versão que faz esse recurso se comportar como no restante da aplicação.28
A 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 consiste em uma única refatoração com um efeito desproporcional. Dependant, o objeto interno que a FastAPI cria para cada nó do grafo de dependências de cada rota, acumulava atributos functools.cached_property desde a versão 0.121.0 (3 de novembro de 2025) — dez deles até a 0.139.2. Uma propriedade em cache precisa de um __dict__ por instância para armazenar seu resultado, portanto o custo se multiplicava por todos os nós de todos os grafos da aplicação. O PR #16049 move essa lógica para fora da classe, colocando-a em 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 própria execução do CodSpeed da FastAPI no PR incorporado aponta que o benchmark de memória test_dependency_graph passou de 17,5 MB para 1,1 MB, uma redução de 16×; o relato que motivou o trabalho descrevia um serviço em produção que se mantinha abaixo de aproximadamente 400 MB na versão 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 e 0.139.2 — continham essa regressão. Se sua aplicação tem uma árvore de dependências profunda ou ampla (Depends() aninhados, esquemas de segurança ou muitos roteadores incluídos), a versão 0.140.0 oferece uma redução de memória sem exigir nenhuma alteração no código da aplicação.27
A 0.140.0 foi o primeiro passo, não a correção completa — fixe a versão 0.140.7 ou posterior. Três dias depois desse lançamento, em 27 de julho de 2026, a FastAPI lançou mais sete versões em cinco horas e meia: da 0.140.1 à 0.140.7, todas elas refatorando o mesmo mecanismo de dependências. O trabalho se divide em duas partes. Primeiro, a árvore simples de dependências: a FastAPI criava e mantinha uma cópia linearizada do grafo de dependências de cada rota, e a versão 0.140.2 deixa de mantê-la, enquanto as versões 0.140.3, 0.140.5, 0.140.6 e 0.140.7 removem os locais restantes que reconstruíam uma dessas cópias — geração de OpenAPI, campos de corpo, parâmetros de solicitação e OpenAPI novamente. A 0.140.4 elimina controles que rastreavam dependências repetidas, mas não eram lidos por nada. Segundo — e esta é a única mudança com um limite visível —, a 0.140.1 aumenta o lru_cache dos helpers de classificação de callables em fastapi/dependencies/models.py de 1.024 para 4.096 entradas, usando uma constante nomeada _CALLABLE_CLASSIFICATION_CACHE_SIZE, porque usuários relataram que aplicações com mais de 1.024 dependências distintas causavam invalidações excessivas no cache. Nada disso altera uma API que você chama, portanto basta atualizar a versão. Vale mencionar claramente duas ressalvas: a cadência indica que essa linha ainda está evoluindo, então leia as notas de versão em vez de presumir que a 0.140.7 é o fim; e a FastAPI adicionou os benchmarks de dependências de OpenAPI que medem esse trabalho no mesmo período (PR #16075), portanto os números publicados abrangem as últimas versões, não todo o arco de sete versões.29
Operações limitadas pela CPU (renderização de Markdown e extração de CSS) podem usar funções síncronas. A FastAPI as executa automaticamente em um pool de threads quando o handler da 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, use async. Se ela executa 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
Middleware de headers de segurança
blakecrosley.com implementa headers de segurança reforçados via middleware customizado:
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 exige isso para avaliação de expressões. A alternativa é o build compatível com CSP do Alpine.js, que tem limitações.14 Todos os outros recursos ficam bloqueados: frame-ancestors impede clickjacking, form-action restringe envios de formulários à mesma origem, e upgrade-insecure-requests força HTTPS.
Segurança de cache em CDN com HTMX
O middleware de headers de segurança adiciona Vary: HX-Request às respostas do 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 header, uma CDN poderia armazenar em cache uma resposta de fragmento do HTMX e servi-la como página completa para uma requisição que não é do HTMX (ou o contrário). O header Vary informa à CDN para armazenar entradas de cache separadas com base no valor do header HX-Request.11
Proteção contra CSRF
Formulários do HTMX usam tokens CSRF sem estado 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 um global do Jinja2 e incluído nas requisições de formulário do 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ões 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 timing.15
Sanitização de HTML
Conteúdo gerado por usuários passa pelo nh3 antes da renderização:
templates.env.filters["sanitize"] = sanitize_html
# In templates: {{ content | sanitize }}
A biblioteca nh3 remove tags e atributos que não estão na allowlist. 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
Modelos Pydantic validam toda entrada na fronteira do 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 queries parametrizadas no banco de dados (SQLAlchemy nunca interpola strings), isso impede SQL injection e garante segurança de tipos nas fronteiras.
Performance
Lighthouse 100/100/100/100
blakecrosley.com pontua 100 nas quatro categorias do Lighthouse: Performance, Accessibility, Best Practices e SEO. Verifique no 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() adiciona 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 print-media, sem carregamento baseado em JavaScript. O arquivo CSS tem cerca de 8 KB com gzip, pequeno o suficiente para que a abordagem de uma única requisição marque 100 em Lighthouse Performance sem malabarismos de otimização.
Compressão GZip
app.add_middleware(GZipMiddleware, minimum_size=500)
Respostas acima de 500 bytes são compactadas. HTML compacta 70-80%, reduzindo um documento de 15 KB para 3-4 KB.
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 com hash de conteúdo (?v=a3f8b2c1d0) são armazenados 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 o parsing de HTML, mas os executa depois que o documento é analisado. Isso evita bloqueio de renderização sem a complexidade de carregamento async e gerenciamento de ordem de execução.
Otimização de imagens
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>
Atributos explícitos de width e height impedem Cumulative Layout Shift (CLS). O atributo loading="lazy" adia imagens fora da tela. WebP gera arquivos 25-35% menores 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 header Link com rel=preload informa ao Cloudflare para enviar uma resposta 103 Early Hints, permitindo que o navegador comece a buscar o CSS antes que o servidor termine de gerar a resposta HTML.17
JavaScript mínimo
A pegada total de JavaScript:
| Biblioteca | Tamanho (minificado + 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 100-300 KB de JavaScript de 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-07-29 | FastAPI 0.141.0 + 0.141.1 (ambas em 29 de julho). A versão 0.141.0 adiciona app.frontend(check_dir="auto"), para que fastapi dev não falhe mais quando o diretório de build estiver ausente — o caso comum de iniciar o servidor antes da execução do build do frontend. Horas depois, a versão 0.141.1 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 na montagem do frontend, embora funcionasse corretamente nas rotas API, fechando uma lacuna real no suporte a dependências adicionado na versão 0.139.0. Ambas foram incorporadas à explicação existente sobre app.frontend(), em vez de receberem uma nova seção, pois a abordagem renderizada no servidor deste guia não monta um diretório dist/. A versão 0.141.1 também documenta FASTAPI_ENV no guia de CLI do FastAPI (somente documentação, sem alteração no corpo do guia). |
28 |
| 2026-07-27 | O FastAPI lançou da versão 0.140.1 à 0.140.7 em cinco horas e meia no dia 27 de julho — sete versões, todas com refatorações do mecanismo de dependências iniciado na 0.140.0. Duas frentes: a cópia achatada do grafo de dependências que o FastAPI criava e mantinha foi removida (0.140.2), junto com todos os pontos restantes que reconstruíam uma — geração de OpenAPI (0.140.3, 0.140.7), campos do corpo (0.140.5) e parâmetros de requisição (0.140.6) —, enquanto a versão 0.140.4 elimina controles internos não utilizados para rastrear repetições. A única alteração com um limite visível é a 0.140.1: o lru_cache dos auxiliares de classificação de callables em fastapi/dependencies/models.py passa de 1.024 para 4.096 entradas (com o nome _CALLABLE_CLASSIFICATION_CACHE_SIZE), após relatos de que aplicativos com mais de 1.024 dependências distintas causavam substituições contínuas no cache. Nenhuma alteração em API; a recomendação no parágrafo sobre memória de dependências passa da versão 0.140.0 para a 0.140.7 ou posterior, com a observação de que essa linha ainda está evoluindo e de que os benchmarks de dependências do OpenAPI (PR nº 16075) só chegaram na última versão da série. |
29 |
| 2026-07-25 | O FastAPI 0.140.0 (24 de julho, 21h16 UTC) corrige uma regressão de memória no sistema de dependências que existia desde a versão 0.121.0 (3 de novembro de 2025). O PR nº 16049 remove dez atributos functools.cached_property de Dependant, transfere-os para auxiliares no nível do módulo e transforma a classe em uma @dataclass(slots=True); a execução oficial do CodSpeed no PR integrado mostra o benchmark de memória test_dependency_graph caindo de 17,5 MB para 1,1 MB (×16), e o relato original descrevia OOM em produção com a versão 0.121.3, enquanto a 0.120.4 permanecia abaixo de aproximadamente 400 MB. Foi adicionada uma passagem sobre a versão 0.140.0 a Padrões assíncronos e uma linha sobre memória de workers à Configuração do Uvicorn para produção. Também foi corrigido um erro preexistente: o guia afirmava que a versão 0.137.0 “fixava o Starlette na linha 1.x” — isso não acontece. O requisito de runtime do FastAPI é starlette>=0.46.0 (um limite mínimo sem limite máximo, ainda atendido pelo Starlette 0.4x) de maneira uniforme nas versões 0.136.3, 0.137.0, 0.138.0, 0.139.2 e 0.140.0; os números 1.x nas notas da versão 0.137.0 são atualizações do dependabot no arquivo de lock de testes do repositório (o PR nº 15722 altera somente uv.lock). A afirmação no corpo e 24 foram corrigidas. Duas não alterações sinalizadas: os componentes internos de Dependant agora causam incompatibilidades para ferramentas (oauth_scopes, cache_key, _uses_scopes e _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 impede monkey-patching de instâncias) — uma API interna e não documentada que este guia nunca menciona, na mesma categoria da alteração de router.routes na versão 0.137.0; e a documentação oficial do FastAPI agora adota projetos uv como padrão, em vez de pip/venv, em 30 arquivos, incluindo README, index.md, virtual-environments.md e as páginas de Docker/implantação (PR nº 16032, integrado em 21 de julho). A alteração na documentação é apenas estética para o seu código, mas o guia ensina pip install -r requirements.txt do início ao fim e agora diverge da principal orientação do projeto upstream — uma decisão editorial futura, deliberadamente não tomada nesta revisão. |
27 |
| 2026-07-24 | htmx 4.0.0-beta6 substitui a beta5 como tag next do npm (publicada em 23 de julho de 2026; versão GitHub no mesmo dia). Principais novidades da beta: nova extensão hx-multipart (respostas multipart/mixed/multipart/parallel transmitidas por streaming, com cabeçalhos de ação HX-* por parte), restauração da posição de rolagem do histórico por meio da API de navegação, com fallback para Firefox, renomeação de evento interno da beta de htmx:swap:finally para htmx:finally:swap, eventos do cabeçalho de resposta HX-Trigger agora disparados após o swap, métodos de requisição personalizados e uma reescrita de hx-ws com encaminhamento de protocols. A recomendação permanece igual — a produção deve continuar na versão HTMX 2.x (latest = 2.0.10) até o lançamento GA da 4.0; a renomeação só introduz incompatibilidade dentro da linha beta da 4.0. A observação sobre a linha beta e 21 foram atualizadas. Foi confirmado que FastAPI 0.139.2, Uvicorn 0.51.0, Alpine.js 3.15.12, Starlette 1.3.1 e Jinja2 3.1.6 permanecem sem alterações; nenhum aviso de segurança no período. |
|
| 2026-07-17 | FastAPI 0.139.1 + 0.139.2 (16 de julho): correção de caminhos com pontos nos fallbacks de app.frontend() (/users/john.doe, PR nº 16011) e construção thread-safe das rotas do router em testes com threads paralelas (PR nº 16013) — nenhuma alteração na API voltada ao aplicativo. Uvicorn 0.49.0 → 0.51.0: a implementação legada de websockets foi descontinuada, e auto agora usa websockets-sansio por padrão (0.50.0); a implementação padrão exige websockets>=13.0 (0.50.2); e a versão 0.51.0 (8 de julho) adiciona reinicializações de workers via SIGHUP com sobreposição, permitindo recarregamentos com tempo de inatividade próximo de zero. O repositório agora fica em Kludex/uvicorn. Foi confirmado que 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 e Bootstrap 5.3.8 permanecem sem alterações; nenhum aviso de segurança no período. |
|
| 2026-07-07 | htmx 4.0.0-beta5 agora é a tag next do npm (publicada em 26 de junho de 2026), substituindo a beta4; a observação sobre a linha beta 4.0 do HTMX e [^22] foram atualizadas de acordo. A recomendação permanece igual — projetos em produção devem continuar na versão HTMX 2.x (latest = 2.0.10) até o lançamento GA da 4.0. Verificado com as dist-tags do npm em htmx.org. |
|
| 2026-07-02 | FastAPI 0.139.0 (1º de julho). app.frontend() agora aceita dependências — por exemplo, autenticação automática por cookie para o frontend servido (PR nº 15908) —, ampliando a montagem de frontend estático da versão 0.138.0 com o mecanismo padrão de Depends(); ainda é independente da proposta deste guia de renderização no servidor e foi mencionado no mesmo parágrafo de comparação. Nenhuma outra mudança na stack: HTMX 2.0.10, Alpine.js 3.15.12, Bootstrap 5.3.8 e SQLAlchemy 2.0.51 permanecem inalterados. |
26 |
| 2026-06-22 | FastAPI 0.138.0 + 0.137.2. A versão 0.138.0 (20 de junho) adiciona app.frontend("/", directory="dist") / router.frontend(...) para servir um frontend estático compilado (saída dist/ de uma SPA) — algo independente da proposta deste guia de renderização no servidor sem etapa de build e mencionado como contraste na seção Padrões assíncronos. A versão 0.137.2 (18 de junho) adiciona iter_route_contexts() como a forma compatível de enumerar rotas agora que router.routes é interno (desde a versão 0.137.0). Ambas adicionam recursos, sem alterações incompatíveis; Starlette (1.3.1), Pydantic (2.13.4), HTMX (2.0.10), Alpine.js (3.15.12), Bootstrap (5.3.8) e SQLAlchemy (2.0.51) permanecem inalterados. |
25 |
| 2026-06-16 | FastAPI 0.137.0/0.137.1 + Starlette 1.0→1.3.1. O FastAPI 0.137.0 (14 de junho) refatora os componentes internos do router: router.routes agora é uma árvore interna, não uma lista simples de APIRoute (causando incompatibilidade em qualquer código que a percorra), ao mesmo tempo que permite rotas adicionadas após include_router() e novos hooks APIRouter.matches()/.handle(); a versão 0.137.1 (15 de junho) corrige a tipagem de APIRoute e routers sem prefixo com caminho vazio. O Starlette lançou sua primeira versão 1.0 estável (22 de março) e agora está na versão 1.3.1 (12 de junho), removendo os hooks descontinuados on_event/on_startup/on_shutdown e os decoradores @app.route()/@app.websocket_route() — lifespan e Route/WebSocketRoute são as únicas opções. (Esta entrada afirmava originalmente que o FastAPI 0.137.0 fixava o Starlette 1.3.1 — corrigido em 2026-07-25: isso não acontece; o requisito de runtime é starlette>=0.46.0, sem limite máximo.) Foi adicionada uma observação sobre lifespan/router à seção Padrões assíncronos. O SQLAlchemy 2.0.51 (15 de junho) contém somente correções de bugs. |
24 |
| 2026-06-08 | Alteração na instalação assíncrona do SQLAlchemy 2.0.50. A partir do SQLAlchemy 2.0.50, a dependência greenlet da stack assíncrona deixou de ser instalada por padrão — instale o extra sqlalchemy[asyncio] (caso contrário, o primeiro await executado no engine falhará com um erro de greenlet ausente). A versão 2.0.50 também exige Python 3.10 ou posterior (o suporte às versões 3.7–3.9 foi removido) e adiciona wheels para a versão 3.13t free-threaded. Foi adicionada uma observação sobre instalação à seção SQLAlchemy 2.0 assíncrono. Nenhuma alteração no corpo para o restante da stack: a versão mais recente do FastAPI continua sendo a 0.136.3 (23 de maio de 2026, sem lançamento em junho), a versão estável do htmx permanece 2.0.10 (a 4.0.0-beta4, “The Fetchening”, está em beta, com lançamento estável previsto aproximadamente para o início de 2027, e ainda não é recomendada para produção), Alpine.js 3.15.12 e Bootstrap 5.3.x permanecem inalterados. A recomendação para produção não mudou: HTMX 2.x até a versão 4.0 se tornar estável.23 |
|
| 2026-05-24 | Verificação de manutenção: o inventário local de conteúdo ainda mostra 210 posts no blog, 11 guias principais, 48 estudos de design e 10 localidades compatíveis, incluindo inglês. A versão mais recente do FastAPI é a 0.136.3 (23 de maio de 2026); a única refatoração voltada ao aplicativo destacada nas notas de lançamento é o tratamento mais rigoroso de cabeçalhos com sublinhados quando convert_underscores=True, enquanto a versão 0.136.2 valida campos de Server-Sent Events para evitar dados de eventos corrompidos. A versão estável do htmx continua sendo a 2.0.10, enquanto a tag next do npm e a documentação da versão 4.0 agora apontam para a 4.0.0-beta4; a versão 2.0 mais recente do SQLAlchemy é a 2.0.50; a versão mais recente do Pydantic permanece a 2.13.4. A recomendação para produção não mudou: use HTMX 2.x até a versão 4.0 se tornar estável.122 |
|
| 2026-05-18 | Atualização do inventário do site: o inventário local de conteúdo agora mostra 210 posts no blog, 11 guias principais, 48 estudos de design e 10 localidades compatíveis, incluindo inglês. A versão mais recente do FastAPI permanece a 0.136.1; a versão estável do htmx continua sendo a 2.0.10, com a tag next do npm na 4.0.0-beta3; a versão mais recente do Alpine.js no npm permanece a 3.15.12. A recomendação para produção não mudou: use HTMX 2.x até a versão 4.0 se tornar estável.12021 |
|
| 2026-05-15 | Verificação de manutenção: a versão mais recente do FastAPI permanece a 0.136.1; este ambiente local do site importa FastAPI 0.128.0 e Starlette 0.50.0; a versão estável do htmx continua sendo a 2.0.10, e a tag next do npm agora é a 4.0.0-beta3; a versão mais recente do Alpine.js no npm é a 3.15.12; a versão mais recente do Bootstrap é a 5.3.8; a versão 2.0 mais recente do SQLAlchemy é a 2.0.49; a versão mais recente do Pydantic é a 2.13.4. A recomendação para produção não mudou: use HTMX 2.x até a versão 4.0 se tornar estável.2021 |
|
| 2026-05-09 | Acompanhamento do htmx 4.0.0-beta3 (8 de maio de 2026): o htmx 4.0.0-beta3 está disponível na tag next do npm e na documentação da versão 4.0, enquanto a tag latest do npm permanece na versão 2.0.10. Destaques que merecem acompanhamento antes do lançamento GA: nova extensão hx-live (expressões reativas ao DOM), nova extensão hx-nonce (proteção por nonce de CSP para atributos htmx) e alterações no guia de migração relacionadas a configurações, histórico, eventos e auxiliares principais de JavaScript. A recomendação para produção não mudou: htmx 2.x permanece na tag mais recente do npm e é a versão recomendada até o lançamento GA da 4.0.21 |
|
| 2026-05-07 | Verificação de manutenção: a versão mais recente do FastAPI permanece a 0.136.1; a versão estável do htmx é a 2.0.10, e a v4 continua em beta, com lançamento previsto para o verão de 2026; a versão mais recente do Alpine.js no npm é a 3.15.12; a versão mais recente do Bootstrap é a 5.3.8; a versão 2.0 mais recente do SQLAlchemy é a 2.0.49; a versão mais recente do Pydantic é a 2.13.4. As métricas locais do site foram atualizadas para 182 posts no blog, 11 guias, dez localidades 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 versão 4.0 se tornar estável.20 | |
| 2026-04-25 | FastAPI 0.136.1 (23 de abril de 2026): limpeza de recursos descontinuados do Pydantic v2 (sem mudanças de comportamento no código do aplicativo). Cronograma do HTMX 4.0 acompanhado: htmx 4.0.0-beta1 (6 de abril) e 4.0.0-beta2 (14 de abril) foram lançados. A orientação de migração não mudou — htmx 2.x permanece na tag mais recente do npm até a versão 4.0 se tornar estável; as correções de segurança continuam, sem urgência para atualizar. Principais alterações da versão 4.0 que já devem ser consideradas no design: (1) fetch() substitui XMLHttpRequest como infraestrutura principal de ajax, (2) a herança de atributos passa a ser explícita por padrão, (3) o suporte ao histórico faz uma requisição de rede para obter o conteúdo restaurado (sem snapshot local do DOM). O FastAPI 0.135.4 (16 de abril) removeu o decorador de Primeiro de Abril @app.vibe() que havia sido incluído na versão 0.135.3. |
|
| 2026-04-16 | Adicionada referência antecipada à versão beta 4.0 do HTMX. Registrado o suporte do FastAPI 0.136.0 a builds free-threaded do Python 3.14t. Recursos do Pydantic 2.13.x (fábricas de valores padrão para atributos privados com acesso aos dados validados do modelo, namespace pydantic.v1 atualizado para 1.10.26 com suporte à versão 3.14). Correções do Alpine.js 3.15.11: modificador x-anchor.noflip, aviso de múltiplos elementos raiz em x-for e correção de regressão de morph em $refs. |
|
| 2026-03-24 | Publicação inicial |
Referências
Este guia aborda todo o sistema usado para criar o blakecrosley.com. O manifesto No-Build apresenta o argumento filosófico. O post Pontuação perfeita no Lighthouse documenta a jornada de otimização de desempenho. O post Vibe Coding vs. engenharia explora como o desenvolvimento assistido por IA se encaixa nesse 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, conteúdo em inglês e mais 9 idiomas traduzidos, dependências Python mínimas e nenhuma ferramenta de build. Verificado no inventário de conteúdo local, 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 alcançava 100/100/100/100 (desempenho, acessibilidade, práticas recomendadas e SEO) em março de 2026. Os resultados podem ser verificados publicamente. Consulte De 76 a 100: como alcançar uma pontuação perfeita no Lighthouse para conhecer toda a jornada de otimização. ↩↩↩
-
Uma instalação nova com
npx create-next-app@latest(Next.js 15, testado em fevereiro de 2026) instala 311 pacotes emnode_modules/, totalizando 187 MB. Projetos em produção com dependências adicionais tendem a ocupar mais espaço. Cada projeto pode variar. Fonte: testes do autor, documentados em O manifesto No-Build. ↩ -
A documentação de desempenho do Next.js da Vercel recomenda otimizações específicas (otimização de imagens, carregamento de fontes e divisão de código) para alcançar 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 no
requirements.txtdo blakecrosley.com em maio de 2026. Atualmente, o arquivo contém 17 entradas de requisitos Python e nenhuma ferramenta de build, compilador ou bundler. ↩ -
Com base na experiência do autor com a manutenção de projetos Next.js (2021–2024), o ecossistema JavaScript gera de 15 a 25 PRs do Dependabot por mês em projetos ativos, a maioria atualizando dependências transitivas que o desenvolvedor nunca importou diretamente. ↩
-
Tim Berners-Lee definiu a compatibilidade retroativa como um princípio de design da web: “um navegador deve ser retrocompatível”. Uma página de 1996 é renderizada no Chrome 2026. Consulte w3.org/DesignIssues/Principles. ↩
-
A OWASP recomenda desabilitar os 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 rotas, parâmetros e modelos de resposta. ↩ -
Documentação do FastAPI sobre handlers async e sync: fastapi.tiangolo.com/async/. Combinar
awaitcom chamadas bloqueantes em funçõesasyncsobrecarrega o loop de eventos. ↩ -
O nh3 é um sanitizador de HTML baseado em Rust e sucessor da biblioteca Bleach. Ele é mantido pelo projeto PyO3 e oferece sanitização de HTML baseada em uma lista de permissões. 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 da requisição. SemVary: HX-Request, uma CDN poderia servir um fragmento HTMX como 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 em todo o mundo. Elas participam da cascata, são herdadas e respondem a media queries em tempo de execução — recursos que as variáveis de pré-processadores não oferecem. Fonte: caniuse.com/css-variables. ↩
-
Documentação do Google sobre hreflang: developers.google.com/search/docs/specialty/international/localized-versions. O valor
x-defaultdefine a página alternativa para usuários cujo idioma não está na lista de hreflang. ↩ -
O 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 esse requisito, mas apresenta limitações. Consulte alpinejs.dev/advanced/csp. ↩ -
Tokens CSRF baseados em HMAC seguem o padrão “Signed Double-Submit Cookie” descrito no guia OWASP CSRF Prevention Cheat Sheet.
hmac.compare_digestusa comparação em tempo constante para evitar ataques de canal lateral baseados em tempo. Consulte cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html. ↩ -
O WebP gera arquivos de 25% a 35% menores que o JPEG com qualidade visual equivalente. Estudo do Google sobre WebP: developers.google.com/speed/webp/docs/webp_study. ↩
-
O 103 Early Hints permite que o servidor (ou a CDN) envie uma resposta preliminar com dicas de preload antes que a resposta final esteja pronta. O Cloudflare oferece suporte a Early Hints para cabeçalhos
Linkcomrel=preload. Consulte developer.chrome.com/blog/early-hints. ↩ -
React 18 + ReactDOM ocupam aproximadamente 42 KB após minificação + gzip. Com um roteador, 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 de retrocompatibilidade do HTMX estão documentados em htmx.org/migration-guide-htmx-1/. Carson Gross apresentou o princípio da retrocompatibilidade em Hypermedia Systems (2023), de Gross, Stepinski e Cotter: hypermedia.systems. ↩
-
Verificação de manutenção em 15 de maio de 2026. O PyPI e as notas de versão do FastAPI indicam 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 indica a versão 2.0.10 no início rápido;
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 indicam a versão 5.3.8; o PyPI e a documentação do SQLAlchemy indicam a versão 2.0.49; o PyPI do Pydantic indica a versão 2.13.4. ↩↩↩↩ -
htmx 4.0.0-beta6 é a versão atual da tag
nextno npm (publicada em 23 de julho de 2026; a linha beta passou por beta3 em 8 de maio de 2026 → beta4 → beta5 → beta6), enquanto a taglatestdo npm permanece na versão 2.0.10. A documentação da versão 4.0 em four.htmx.org acompanha o buildnext, o índice de extensões da versão 4.0 listahx-liveehx-nonce, e o guia de migração da versão 4.0 documenta as mudanças que devem ser analisadas antes de migrar aplicações em produção da versão 2.x. Verificado nas dist-tags do pacotehtmx.orgno npm em 24 de julho de 2026. ↩↩↩↩↩↩ -
Verificação de manutenção em 24 de maio de 2026. Os comandos de inventário local retornaram 210 posts de blog em Markdown, 11 arquivos de guias de nível superior e 48 arquivos de estudos de design. As notas de versão do FastAPI indicam a versão 0.136.3 em 23 de maio de 2026, com tratamento mais rigoroso de sublinhados em cabeçalhos quando
convert_underscores=True; a versão 0.136.2 valida campos de Server-Sent Events.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; a documentação de instalação do four.htmx.org mostrahtmx.org@4.0.0-beta4. ↩↩ -
Changelog do SQLAlchemy 2.0.50 e post de lançamento, publicado em 24 de maio de 2026. A dependência
greenletdo asyncio não é mais instalada por padrão; agora, o destino de instalaçãosqlalchemy[asyncio]é obrigatório para incluí-la. A versão 2.0.50 também encerra o suporte ao Python 3.7/3.8/3.9 (agora exige 3.10+), adiciona wheels para Python sem GIL e adiciona o parâmetro de quadro de janelaover(..., exclude=...). Versão mais recente verificada no PyPI em 8 de junho de 2026. O htmx 4.0.0-beta4 (“The Fetchening”, 22 de maio de 2026) permanece em beta, com lançamento estável previsto para o início de 2027; FastAPI 0.136.3 (23 de maio de 2026), Alpine.js 3.15.12 e Bootstrap 5.3.x não tiveram alterações nesse período. ↩↩↩ -
Notas de versão do FastAPI: a versão 0.137.0 (14 de junho de 2026) refatora os componentes internos do roteador, de modo que
router.routesdeixa de ser uma lista simples de objetosAPIRoutee passa a ser uma árvore de objetos intermediários (trate-a como interna); ela também permite adicionar rotas depois deinclude_router(), incluir um sub-roteador antes que suas rotas sejam definidas, evita copiar rotas e adicionaAPIRouter.matches()/.handle(). Ela 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 — exatamente igual nas versões 0.136.3, 0.137.0, 0.138.0, 0.139.2 e 0.140.0, conforme verificado nos metadadosrequires_distdo API JSON no PyPI em 25 de julho de 2026. A linha “bump starlette from 1.1.0 to 1.2.1” (PR nº 15722) nas notas da versão 0.137.0 é uma atualização do Dependabot na seção Interno que modifica apenas o arquivo de lock de testesuv.lockdo repositório. (Um limite máximo existia anteriormente — as versões 0.120.4 e 0.121.0 foram lançadas comstarlette<0.50.0,>=0.40.0—, mas ele já havia sido removido na versão 0.136.3.) Correção aplicada em 25 de julho de 2026; o texto anterior desta nota de rodapé e a afirmação no corpo estavam errados. A versão 0.137.1 (15 de junho de 2026) corrige a tipagem de APIRoute e um caminho vazio em um roteador sem prefixo. Notas de versão do Starlette: a versão 1.0.0 (22 de março de 2026), seu primeiro lançamento estável em cerca de 8 anos, removeuon_startup/on_shutdown/on_event()e os decoradores@app.route()/@app.websocket_route()(uselifespaneRoute/WebSocketRoute); a versão mais recente é a 1.3.1 (12 de junho de 2026). O SQLAlchemy 2.0.51 (changelog, 15 de junho de 2026) traz apenas correções de bugs, sem impacto em async ou na instalação. Verificado no PyPI e nas notas de versão oficiais em 16 de junho de 2026. ↩↩↩ -
Notas de versão do FastAPI: a versão 0.138.0 (20 de junho de 2026) adiciona
app.frontend("/", directory="dist")erouter.frontend("/", directory="dist")para servir um frontend estático já compilado (PR nº 15800; documentação de frontend) — um recurso para servir uma SPA estática emdist/, não um padrão renderizado no servidor; nenhuma mudança incompatível. A versão 0.137.2 (18 de junho de 2026) adicionaiter_route_contexts()para usos avançados que antes percorriamrouter.routes(interno desde a versão 0.137.0); nenhuma mudança incompatível. Nenhum lançamento posterior à versão 0.138.0 até 22 de junho de 2026. 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 no PyPI e nas notas de versão oficiais em 22 de junho de 2026. ↩↩ -
Notas de versão da versão 0.139.0 do FastAPI, 1º de julho de 2026: “Suporte a dependências em
app.frontend(), por exemplo, para autenticação automática por cookie no frontend” (PR nº 15908). O restante da versão contém traduções, documentação e atualizações de dependências; nenhuma mudança incompatível. Verificação da sessão atual em 2 de julho de 2026 (PST): a versão 0.139.0 é o lançamento mais recente na página de versões do GitHub. ↩↩ -
Notas de versão da versão 0.140.0 do FastAPI, publicada em 24 de julho de 2026 às 21:16 UTC (
upload_time_iso_8601do PyPI:2026-07-24T21:16:42Z). A única entrada de refatoração é “⚡️ Reduzir o uso de memória nas dependências. PR nº 16049” (incorporado em 24 de julho de 2026 às 21:07:52 UTC). A regressão foi introduzida pelo PR nº 14262 (incorporado em 3 de novembro de 2025) e lançada na versão 0.121.0 no mesmo dia, que adicionoufunctools.cached_propertyaDependant.cache_key; na versão 0.139.2, a classe continha dez definições@cached_property. Na versão 0.140.0,fastapi/dependencies/models.pydeclara@dataclass(slots=True) class Dependant, com a lógica transferida para_get_cache_key(),_get_oauth_scopes(),_uses_scopes()e_is_security_scheme()no nível do módulo — código-fonte verificado na tag 0.140.0. O bot CodSpeed no PR incorporado informa que o benchmark de memóriatest_dependency_graphpassou de 17,5 MB (base) para 1,1 MB (head), uma “melhoria de desempenho de ×16”; a versão 0.140.0 também adiciona um benchmark de memória à CI (PR nº 16046) para evitar que a regressão volte a ocorrer. O relato original é a discussão nº 14742, na qual a versão 0.120.4 permaneceu abaixo de aproximadamente 400 MB e a versão 0.121.3 sofreu OOM em produção. Observação para autores de ferramentas:Dependant.oauth_scopes,.cache_key,._uses_scopese._is_security_schemedeixaram de existir como atributos, eslots=Trueimpede monkey-patching de instâncias — um API interno não documentado que este guia não usa, na mesma categoria da mudança emrouter.routesda versão 0.137.0. Todos os fatos foram verificados novamente no PyPI, no API do GitHub e no código-fonte da tag em 25 de julho de 2026. ↩↩↩ -
FastAPI 0.141.0 (29 de julho de 2026, 14:47 UTC) adicionou
app.frontend(check_dir="auto")para desenvolvimento local comfastapi dev(PR nº 16102). FastAPI 0.141.1 (29 de julho de 2026, 17:17 UTC) corrigiu o suporte a tarefas em segundo plano e cabeçalhos provenientes de dependências emapp.frontend()(PR nº 16105) e documentouFASTAPI_ENVno guia CLI do FastAPI (PR nº 16104). Ambas por @tiangolo. A versão mais recente no PyPI foi confirmada como 0.141.1 em 29 de julho de 2026. ↩↩ -
Versões do FastAPI, da 0.140.1 à 0.140.7, todas publicadas em 27 de julho de 2026 entre 12:07 e 17:34 UTC (
upload_time_iso_8601do PyPI: 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 texto de cada versão contém uma única entrada de refatoração: 0.140.1 “Atualizar o limite de lru_cache das dependências para comportar aplicações grandes” (PR nº 16062); 0.140.2 “Parar de manter árvores de dependências planificadas” (PR nº 16065); 0.140.3 “Evitar a planificação repetida de dependências no OpenAPI” (PR nº 16067); 0.140.4 “Ignorar o controle de repetições de dependências não utilizadas” (PR nº 16069); 0.140.5 “Evitar planificar dependências para campos do corpo” (PR nº 16071); 0.140.6 “Evitar planificar dependências para parâmetros de requisição, principalmente para OpenAPI” (PR nº 16073); 0.140.7 “Evitar planificar dependências para OpenAPI” (PR nº 16076). O valor do cache vem do diff do PR nº 16062, que substitui três decoradores@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 texto do PR afirma: “Alguns usuários relataram mais de 1.024 dependências; isso deve comportar aplicações maiores.” A versão 0.140.2 também adiciona um benchmark de memória (PR nº 16064), e a versão 0.140.7 adiciona benchmarks de dependências do OpenAPI (PR nº 16075; portanto, a cobertura de benchmarks é posterior à maior parte da série. Verificado na página de versões do API do GitHub, nos diffs dos PRs e no PyPI em 27 de julho de 2026; a versão 0.140.7 era o lançamento mais recente no momento da redação. ↩↩