MÓDULO 05 · CONCEITO 04 DE 14

Middleware HTTP pipeline com next

Rack, WSGI, Connect, ASP.NET Core, ASGI, chi. A convenção que virou universal para empilhar cross-cutting concerns ao redor da request — onion model, short-circuit, e por que ordem é parte do contrato.

Tempo de leitura ~22 min Pré-requisito Conceito 03 (decorator e higher-order functions) Próximo Interceptors e filters

Em fevereiro de 2007, Christian Neukirchen publicou no blog dele um texto curto que apresentava uma especificação de duas páginas chamada Rack. A proposta era trivial: padronizar a interface entre servidores web Ruby (Mongrel, WEBrick, Thin) e frameworks Ruby (Rails, Sinatra, Camping). Cada framework tinha até então conector próprio para cada servidor, e a combinatória virava insustentável. Rack definiu uma convenção minimalista — uma aplicação Rack é um objeto que responde a call(env) e devolve [status, headers, body]. Em três anos, todo framework Ruby falava Rack; em cinco, a ideia tinha viajado para Python (WSGI já existia, mas o estilo composicional Rack influenciou Werkzeug e Django middleware), JavaScript (Connect, depois Express), .NET (OWIN, depois Katana, depois ASP.NET Core), e Go (a interface http.Handler de 2009 já tinha essa filosofia desde a primeira hora).

O que viajou junto com Rack não foi só a interface. Foi também o padrão de empilhamento — middleware, no vocabulário que ficou. Cada camada da pilha recebe a request, opcionalmente transforma, delega para a próxima camada via uma função chamada next (ou call, ou ServeHTTP), e opcionalmente transforma o retorno. A pilha inteira é montada uma vez no startup, e cada request atravessa toda ela na ida e na volta. É decorator do conceito anterior, aplicado ao caso particular de processamento HTTP.

Em 2026, a convenção é tão universal que parece ter sido sempre assim. ASP.NET Core, FastAPI, Express, chi, gin, actix-web, Phoenix em Elixir, Spring WebFlux, Axum em Rust — todos usam a mesma estrutura de pipeline com next. Vale entender o porquê do sucesso e os detalhes que a literatura casual ignora — especialmente ordem de empilhamento, fluxo de retorno, e short-circuit, que são onde mais se erra na prática.

Este conceito formaliza o middleware como caso particular de decorator e mostra a anatomia em três frameworks contemporâneos. Os concerns específicos que mais aparecem em pipelines — logging, observabilidade, auth, retry, cache — vêm nos conceitos seguintes; aqui o foco é o pipeline em si, sua mecânica e suas armadilhas.

O onion model — request entrando, response saindo

A imagem mais usada para descrever middleware é a da cebola. Imagine uma cebola com várias camadas concêntricas. A request chega na camada mais externa, atravessa as camadas em direção ao centro, atinge o handler de domínio (o "miolo"), e a response viaja de volta atravessando as mesmas camadas em ordem inversa. Cada camada — cada middleware — pode atuar tanto na ida (antes de chamar next) quanto na volta (depois de next retornar).

Essa estrutura tem três consequências práticas que merecem atenção. Primeira: ordem importa, e a ordem é simétrica — o que entra primeiro sai por último. Um middleware de logging registrado antes do middleware de auth vê requests não-autenticadas; registrado depois, só vê as que passaram pela auth. Isso é decisão de design, não detalhe de implementação.

Segunda: cada camada pode interromper o fluxo retornando sem chamar next. Esse é o short-circuit — middleware de auth retorna 401 direto sem delegar para a camada interna; middleware de cache retorna response cacheada sem reexecutar o handler; rate limiter retorna 429 e nunca toca o domínio. Short-circuit é o que diferencia middleware de chain genérico — não é só transformar dado e passar adiante, é poder não passar adiante.

Terceira: o que acontece depois de next só executa se o miolo (e todas as camadas internas) retornarem sem exceção não-tratada. Isso significa que o lugar certo para tratar erro genericamente é uma camada externa que envolve next em try, e o lugar certo para registrar duração total é o middleware mais externo possível. Posicionamento errado desse tipo de concern produz métrica que não captura erro, ou log de erro que vaza por cima do tratamento genérico.

A mecânica em ASP.NET Core

ASP.NET Core formalizou middleware na sua reescrita de 2016 após a abordagem OWIN/Katana de 2013. O modelo central é uma RequestDelegate — função que recebe HttpContext e retorna Task. Cada middleware é uma função que recebe a próxima RequestDelegate da cadeia e retorna outra que compõe comportamento próprio com a delegação.

// Program.cs — ASP.NET Core 8/9/10
var app = WebApplication.CreateBuilder(args).Build();

// middleware "lambda" inline
app.Use(async (HttpContext ctx, RequestDelegate next) => {
    var sw = Stopwatch.StartNew();
    await next(ctx);                       // delega para o próximo
    var ms = sw.ElapsedMilliseconds;
    app.Logger.LogInformation(
        "request {Method} {Path} {Status} {Ms}ms",
        ctx.Request.Method, ctx.Request.Path,
        ctx.Response.StatusCode, ms);
});

// middleware com classe + UseMiddleware (preferível em produção)
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<ExceptionHandlerMiddleware>();

// middleware embutido do framework
app.UseAuthentication();
app.UseAuthorization();

// roteamento e endpoints (o "miolo")
app.MapControllers();

await app.RunAsync();

A ordem em que se chama app.Use é a ordem de empilhamento. ASP.NET Core documenta uma ordem recomendada explicitamente — exception handler primeiro (mais externo), depois HSTS, HTTPS redirection, static files, routing, CORS, authentication, authorization, custom, endpoint. Cada item dessa lista tem motivo: exception handler precisa estar fora para capturar tudo; static files precisa estar antes de routing para não rodar pipeline completo para arquivo estático; CORS antes de auth porque OPTIONS preflight não tem credencial; auth antes de authorization porque você não pode autorizar sem identificar.

Em produção, prefere-se classe sobre lambda — torna o middleware testável isoladamente, injeta dependências via construtor, e permite reutilização. A interface IMiddleware existe para isso desde 2.1, mas muitos times usam o padrão "convencional" (classe com método InvokeAsync), que é mais flexível porque permite DI per-request.

ASGI e o middleware Python moderno

Python tem uma história mais sinuosa. WSGI (PEP 3333, 2010, revisão de PEP 333 de 2003) foi a primeira convenção composicional pythônica e influenciou Rack — mas WSGI é síncrono e não suporta WebSocket nem long polling. ASGI, especificado por Andrew Godwin entre 2016 e 2019 (versão 3.0 atual), é o sucessor async-first. Frameworks modernos — FastAPI, Starlette, Django desde 3.0 — falam ASGI nativo.

Middleware ASGI segue a mesma estrutura de Rack/WSGI: um callable que recebe scope, receive, send e a próxima aplicação ASGI. FastAPI dá sintaxe mais alta — você registra middleware via app.middleware("http") ou app.add_middleware(...), e o framework lida com o protocolo ASGI por baixo.

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import time
import structlog

log = structlog.get_logger()
app = FastAPI()

# middleware via decorator (sintaxe FastAPI)
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
    started = time.perf_counter()
    response = await call_next(request)            # equivalente a next()
    elapsed_ms = (time.perf_counter() - started) * 1000
    log.info(
        "request",
        method=request.method,
        path=request.url.path,
        status=response.status_code,
        duration_ms=round(elapsed_ms, 2),
    )
    return response

# middleware via classe ASGI (mais portável entre frameworks)
class CorrelationIdMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        cid = request.headers.get("X-Correlation-ID") or new_id()
        request.state.correlation_id = cid
        response = await call_next(request)
        response.headers["X-Correlation-ID"] = cid
        return response

# composição (ordem de add é ordem de empilhamento — primeiro adicionado = mais externo)
app.add_middleware(CorrelationIdMiddleware)
app.add_middleware(CORSMiddleware, allow_origins=["*"])

Atenção a uma sutileza importante de FastAPI/Starlette: add_middleware empilha em ordem inversa à intuição. O primeiro middleware adicionado é o mais externo, e o último adicionado é o mais próximo do handler. Isso bate com o modelo de Starlette por baixo (cada middleware "embrulha" a app que já existia), mas confunde quem vem do ASP.NET Core, onde a ordem de Use é a ordem em que a request encontra cada camada. Documentação interna que explicita a ordem efetiva é prática que evita bug.

Go — a convenção func(http.Handler) http.Handler

Go nunca teve framework HTTP dominante porque a biblioteca padrão entrega a parte difícil. net/http define http.Handler como interface mínima, e a comunidade convergiu na convenção func(http.Handler) http.Handler para middleware — exatamente o decorator do conceito anterior aplicado ao caso HTTP. Bibliotecas como chi, gorilla/mux e echo só fornecem açúcar para essa convenção: router com Use para registrar middleware, sub-routers para escopos diferentes, e helpers para erro e logging.

package main

import (
    "context"
    "log/slog"
    "net/http"
    "time"

    "github.com/go-chi/chi/v5"
    "github.com/google/uuid"
)

type ctxKey string

const correlationKey ctxKey = "correlation_id"

// middleware: correlation id
func WithCorrelationID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        cid := r.Header.Get("X-Correlation-ID")
        if cid == "" {
            cid = uuid.NewString()
        }
        ctx := context.WithValue(r.Context(), correlationKey, cid)
        w.Header().Set("X-Correlation-ID", cid)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// middleware: timing + structured log
func WithTiming(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        rw := &statusRecorder{ResponseWriter: w, status: 200}
        started := time.Now()
        next.ServeHTTP(rw, r)
        slog.InfoContext(r.Context(), "request",
            "method", r.Method,
            "path", r.URL.Path,
            "status", rw.status,
            "duration_ms", time.Since(started).Milliseconds(),
        )
    })
}

type statusRecorder struct {
    http.ResponseWriter
    status int
}

func (s *statusRecorder) WriteHeader(code int) {
    s.status = code
    s.ResponseWriter.WriteHeader(code)
}

func main() {
    r := chi.NewRouter()
    r.Use(WithCorrelationID)
    r.Use(WithTiming)
    // outros middlewares...
    r.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
        w.Write([]byte(`{"status":"ok"}`))
    })
    http.ListenAndServe(":8080", r)
}

Note três detalhes idiomáticos. Primeiro: para capturar o status code da response (que http.ResponseWriter não expõe diretamente), envolve-se a writer em um statusRecorder — um decorator de ResponseWriter, decorator dentro de decorator. Segundo: para passar correlation ID adiante, usa-se context.Context — o conceito 13 do módulo anterior cobre isso em profundidade. Terceiro: ordem de r.Use é ordem de empilhamento "do externo para o interno", como em ASP.NET Core (e diferente de FastAPI). Cultura local.

Short-circuit — quando next não é chamado

Short-circuit é o que torna middleware útil para concerns de segurança e otimização. Os três casos canônicos:

Auth e autorização: middleware verifica token, se inválido escreve 401 ou 403 e retorna sem chamar next. O handler de domínio jamais é executado, e — crucialmente — middlewares posteriores ao auth também não. Se o middleware de log de payload está depois do auth, requests não-autenticadas não vazam payload em log; se está antes, vazam.

Cache de response: middleware verifica chave do cache; se hit, escreve a response cacheada e retorna sem chamar next. O handler interno e todos os middlewares posteriores ao cache não rodam. Isso é vantagem — eficiência — mas é também perigo: métricas que dependem de executar o handler ficam zeradas para hit de cache. Daí a escolha do conceito anterior sobre ordem de Logging(Cached(...)) versus Cached(Logging(...)).

Rate limiting: middleware verifica taxa por cliente; se excedida, escreve 429 e retorna. A mesma lógica de short-circuit. Aqui o cuidado é não rate-limitar por cliente errado — rate limiter antes do middleware que extrai identidade limita por IP; depois, por usuário autenticado.

Tratamento de erro — o middleware mais externo

A regra prática vinda de quase qualquer framework: middleware de tratamento de erro vai na camada mais externa do pipeline. A razão é o onion model — só o try mais externo captura exceção que escapou de qualquer middleware interno. Se o handler de erro está em uma camada interna, exceções lançadas por middleware externo a ele jamais são tratadas.

A forma canônica em ASP.NET Core é app.UseExceptionHandler(...) como primeira chamada. Em FastAPI, é app.add_exception_handler(...) — Starlette posiciona automaticamente como mais externo. Em Go com chi, é convenção registrar o middleware de recover/error-mapper como o primeiro r.Use. O objetivo é o mesmo em todos: garantir que qualquer caminho de exceção, em qualquer middleware ou handler, resulte em response HTTP bem-formada com status apropriado, sem vazar stack trace.

armadilha em produção

Middleware com efeito colateral entre requests. Um caso recorrente é middleware que mantém estado em variável global — contador, mapa de cache, lista. Em servidor concorrente, várias goroutines/threads/tasks executam o middleware simultaneamente, e o estado vira race condition. O sintoma é log esporádico estranho ou métrica que zera sozinha. Regra: middleware deve ser stateless ou usar estrutura concorrente explícita (sync.Map em Go, ConcurrentDictionary em C#, threading.Lock em Python). E nunca, jamais, escrever em variável compartilhada sem sincronização achando que "request é rápida demais para colidir".

O mesmo pipeline, três sintaxes

Para concretizar a equivalência, considere um pipeline mínimo com três middlewares: correlation ID, timing/log, error handler. As três versões abaixo fazem a mesma coisa em três ecossistemas, e dá para ler em paralelo.

C# — ASP.NET Core (ordem natural: Use é externo→interno)
var app = builder.Build();

// camada mais externa (registra primeiro)
app.UseExceptionHandler(errorApp => {
    errorApp.Run(async ctx => {
        ctx.Response.StatusCode = 500;
        await ctx.Response.WriteAsJsonAsync(new { error = "internal" });
    });
});

app.Use(async (ctx, next) => {                         // correlation ID
    var cid = ctx.Request.Headers["X-Correlation-ID"]
        .FirstOrDefault() ?? Guid.NewGuid().ToString();
    ctx.Response.Headers["X-Correlation-ID"] = cid;
    using (LogContext.PushProperty("correlation_id", cid))
        await next();
});

app.Use(async (ctx, next) => {                         // timing
    var sw = Stopwatch.StartNew();
    await next();
    Log.Information("request {Method} {Path} {Status} {Ms}ms",
        ctx.Request.Method, ctx.Request.Path,
        ctx.Response.StatusCode, sw.ElapsedMilliseconds);
});

app.MapControllers();
await app.RunAsync();

Ordem natural: Use chamado primeiro = camada mais externa. Exception handler é o primeiro porque deve envolver tudo. Correlation ID antes de timing porque o log de timing precisa ter o ID em escopo.

Python — FastAPI (ordem invertida: add_middleware empilha de fora para dentro)
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
import time, structlog, uuid

log = structlog.get_logger()
app = FastAPI()

class TimingMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        started = time.perf_counter()
        response = await call_next(request)
        log.info("request",
                 method=request.method, path=request.url.path,
                 status=response.status_code,
                 duration_ms=round((time.perf_counter() - started) * 1000, 2))
        return response

class CorrelationIdMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request: Request, call_next):
        cid = request.headers.get("X-Correlation-ID") or str(uuid.uuid4())
        structlog.contextvars.bind_contextvars(correlation_id=cid)
        try:
            response = await call_next(request)
        finally:
            structlog.contextvars.clear_contextvars()
        response.headers["X-Correlation-ID"] = cid
        return response

# último add = mais interno; primeiro add = mais externo
app.add_middleware(TimingMiddleware)        # mais interno dos dois
app.add_middleware(CorrelationIdMiddleware) # mais externo

@app.exception_handler(Exception)
async def all_errors(request, exc):
    return JSONResponse(status_code=500, content={"error": "internal"})

FastAPI/Starlette empilham na ordem inversa de add_middleware. Para garantir que o correlation ID está em escopo durante o timing, ele precisa ser adicionado depois do timing — porque vira a camada externa. Isso é peculiaridade que merece comentário no código.

Go — chi (ordem natural como ASP.NET Core)
r := chi.NewRouter()

r.Use(middleware.Recoverer)              // mais externo: captura panics
r.Use(WithCorrelationID)
r.Use(WithTiming)

r.Get("/produtos/{id}", h.Obter)

http.ListenAndServe(":8080", r)

chi tem ordem natural: primeiro Use é mais externo. middleware.Recoverer da própria chi captura panic e converte em 500 — equivalente ao exception handler. Para tipos de erro específicos (404, 409), em Go a convenção é o handler retornar erro tipado e um middleware mapper traduzir, em vez de exceção.

Testando middleware isoladamente

Middleware bem desenhado é testável fora do framework HTTP. A técnica em todas as linguagens é a mesma: passar um handler "fake" como next, montar um request sintético, e verificar comportamento — status retornado, headers manipulados, log emitido, contexto propagado.

// Go — teste isolado de WithCorrelationID
func TestCorrelationIDMiddleware_addsHeader(t *testing.T) {
    var receivedCtxID string
    fakeNext := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
        receivedCtxID = r.Context().Value(correlationKey).(string)
    })

    handler := WithCorrelationID(fakeNext)
    rec := httptest.NewRecorder()
    req := httptest.NewRequest(http.MethodGet, "/x", nil)

    handler.ServeHTTP(rec, req)

    if rec.Header().Get("X-Correlation-ID") == "" {
        t.Fatal("expected X-Correlation-ID header to be set")
    }
    if receivedCtxID == "" {
        t.Fatal("expected correlation id propagated via context")
    }
}

A virtude da estrutura: WithCorrelationID não depende de chi, de servidor, de banco. É função pura sobre http.Handler. A mesma estética vale em ASP.NET Core (testar via HttpContext sintético ou WebApplicationFactory) e em FastAPI (testar via TestClient ou montando BaseHTTPMiddleware diretamente). Middleware que precisa de "rodar o servidor" para ser testado é sinal de design ruim.

Anti-padrões frequentes

Middleware blocante em runtime async. Em FastAPI, registrar middleware que faz chamada síncrona de I/O (request HTTP bloqueante, query síncrona ao banco) bloqueia o event loop e prejudica todas as requests concorrentes. Especialmente comum em projetos migrando de Flask. Solução: sempre usar versão async dos clients, e quando absolutamente necessário, embrulhar em asyncio.to_thread.

Middleware que lê o body sem reescrevê-lo. O body de uma request HTTP é stream consumível uma vez. Se um middleware lê o body para inspecioná-lo (logging, validação, hashing) e não o reinjeta, o handler interno encontra body vazio. ASP.NET Core tem EnableBuffering() para isso; FastAPI documenta o problema e oferece request._body como cache; em Go, é preciso substituir r.Body por um io.NopCloser sobre os bytes lidos.

Middleware muito grande. Quando um middleware passa de cinquenta linhas, geralmente está fazendo várias coisas — auth + load do usuário + verificação de feature flag + logging em uma só camada. Quebra em vários middlewares pequenos torna a pilha legível e testável; o custo de cada middleware adicional é mínimo, e o ganho de clareza é alto.

Ordem decidida por trial-and-error. Em times onde ninguém articula a ordem do pipeline, ela vira folclore: "mexe e roda os testes pra ver". Times maduros têm um diagrama do pipeline em algum docs/architecture.md com cada middleware e a justificativa da posição. Investimento de uma hora que paga anos.

heurística do sênior

A ordem do pipeline conta uma história sobre o sistema. Recoverer/exception-handler é o primeiro porque o sistema nunca pode vazar erro cru. Correlation ID vem cedo porque tudo depois precisa carregar o ID. Auth vem antes de autorização, antes de qualquer log de payload, antes de qualquer endpoint de domínio. Métricas e tracing vêm na camada que mede o que importa — geralmente perto do roteamento, mas dentro da auth para não medir requests não-autenticadas como tráfego válido. Cada decisão é articulável. Se você não consegue justificar a posição de cada middleware em uma frase, a ordem está acidental.

Por que importa para a sua carreira

Pipeline de middleware é a primeira coisa que se lê em um sistema novo de aplicação web. Quem entende a estrutura consegue mapear em poucos minutos quais cross-cutting concerns a equipe escolheu, em que ordem aplicou, e onde estão os pontos de extensão. Em entrevista de design, "como você organizaria o pipeline HTTP de uma API nova?" é uma das perguntas mais comuns para vagas backend, e a resposta forte tem três camadas: enumera os concerns (auth, log, métrica, cache, error mapping, rate limit), justifica a ordem com base no onion model, e menciona pelo menos um caso de short-circuit como ferramenta. Em revisão de código, ver pipeline com ordem acidental e propor a reordenação justificada é um dos atos arquiteturais mais altos por unidade de esforço — pequena mudança, alto impacto.

Como praticar

  1. Pipeline mínimo nas três linguagens. Monte em ASP.NET Core, FastAPI e Go (com chi) o mesmo pipeline: recoverer/exception, correlation ID, timing+log, auth fake (qualquer header X-User autentica), rate limit simples (10 req/s por IP). Documente em comentário ou README a ordem escolhida e o motivo de cada posição. Compare quão verbosa fica cada implementação — esse é o exercício que faz você sentir a cultura de cada ecossistema.
  2. Diagnóstico de pipeline existente. Pegue um projeto seu (ou um aberto que você usa) com pipeline HTTP de cinco ou mais middlewares. Liste todos em ordem. Para cada um, escreva: o que faz, por que está nessa posição, o que aconteceria se fosse movido uma posição para cima/baixo. Identifique pelo menos um caso onde a ordem está justificada por inércia, não por design. Proponha a mudança.
  3. Teste isolado de middleware. Escolha um middleware não-trivial — algo que faça mais que adicionar header, idealmente algo que dependa do contexto da request (validação de token, rate limiter por usuário). Escreva teste unitário sem subir o servidor, fornecendo um next fake e uma request sintética. Cubra: caso de sucesso, short-circuit (quando o middleware não chama next), e caminho de erro (quando o next lança exceção). Mais que 80% de cobertura nesse tipo de middleware é mensagem clara de boa decomposição.

Referências para aprofundar

  1. artigo Introducing Rack — Christian Neukirchen (2007). chneukirchen.org/blog/archive/2007/02/introducing-rack.html — O post original que introduziu Rack. Curto, mas fundador. Mostra como uma convenção de duas páginas pode redefinir o ecossistema inteiro.
  2. docs ASP.NET Core Middleware. learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware — Documentação canônica, com a tabela de ordem recomendada do pipeline. Atualizada a cada release. Leitura obrigatória para qualquer dev .NET.
  3. docs ASGI Specification 3.0 — Andrew Godwin (2019). asgi.readthedocs.io — A spec oficial. Definição precisa de scope/receive/send. Lê em uma tarde e cobre o protocolo completo que está por trás de FastAPI, Starlette e Django Channels.
  4. docs FastAPI — Middleware. fastapi.tiangolo.com/tutorial/middleware — Documentação do FastAPI sobre middleware, incluindo a peculiaridade da ordem de add_middleware. Curta, com exemplos práticos.
  5. docs chi — composable router for HTTP services. github.com/go-chi/chi — Documentação completa de chi com seção dedicada a middleware. Código fonte ainda mais didático: o dispatcher de middleware tem ~200 linhas legíveis.
  6. livro ASP.NET Core in Action (3ª ed.) — Andrew Lock (Manning, 2024). Cap. 4 (The application startup process and middleware). Lock explica o pipeline com profundidade rara, incluindo a evolução de OWIN para a forma atual.
  7. livro Architecture Patterns with Python — Harry Percival & Bob Gregory (O'Reilly, 2020). Cobre middleware no contexto de aplicações ASGI com Flask/FastAPI, incluindo a separação entre concerns de borda e domínio. Bom para a perspectiva pythônica.
  8. livro Let's Go Further — Alex Edwards (auto-publicado, 2024). lets-go-further.alexedwards.net — O livro de referência para Go web idiomático. Capítulo de middleware é o tratamento mais pragmático que existe; o estilo do livro inteiro é "como sêniores Go escrevem".
  9. artigo The OWIN Specification — Microsoft / community (2013). owin.org — Spec original do OWIN, predecessor do ASP.NET Core. Útil para entender a transição da arquitetura monolítica do ASP.NET clássico para o modelo composicional moderno.
  10. artigo How does ASP.NET Core middleware really work? — Andrew Lock (blog, 2019). andrewlock.net — Lock destrincha o pipeline de ASP.NET Core mostrando a execução real, incluindo erros comuns. Posts curtos de blog com mais valor por minuto que vários livros.
  11. artigo Why Express middleware is awesome — Evan Hahn (2014). evanhahn.com/why-express-middleware-is-awesome — Texto histórico sobre Connect/Express. Mesmo quem nunca vai escrever Node aprende muito sobre o padrão lendo a articulação dele.
  12. vídeo Mat Ryer — How I write HTTP services in Go (Gophercon, 2019; revisado 2024). grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years — Versão escrita do talk clássico de Ryer, atualizada em 2024. A forma idiomática de organizar middleware e handlers em Go.