---
title: "templ em Go: Componentes HTML Type-Safe com net/http e HTMX"
url: "https://golang.com.br/blog/templ-go-componentes-html-htmx/"
markdown_url: "https://golang.com.br/blog/templ-go-componentes-html-htmx.MD"
description: "Aprenda templ em Go: instalação, componentes type-safe, layouts, net/http, Chi, formulários, HTMX, testes, segurança e comparação com html/template."
date: "2026-07-23"
author: "Golang Brasil"
---

# templ em Go: Componentes HTML Type-Safe com net/http e HTMX

Aprenda templ em Go: instalação, componentes type-safe, layouts, net/http, Chi, formulários, HTMX, testes, segurança e comparação com html/template.


Se você quer construir uma interface web em Go sem levar React, Vue ou outro framework JavaScript para o projeto, **templ** é uma das opções mais práticas do ecossistema atual. Ele transforma arquivos `.templ` em código Go, permite criar componentes HTML com parâmetros tipados e renderiza tudo no servidor. O resultado é um fluxo simples: o handler busca os dados, chama um componente e devolve HTML.

A recomendação direta é esta: use `html/template` quando a aplicação tem poucas páginas e templates tradicionais; avalie **templ** quando a interface começou a exigir componentes reutilizáveis, layouts, estados de loading/erro e integração frequente com HTMX. Você continua no ecossistema Go, ganha verificação no build e não precisa criar uma API JSON só para o frontend consumir.

Este guia mostra instalação, componentes, layouts, integração com `net/http` e [Chi](/blog/frameworks-http-go-gin-echo-fiber-chi/), formulários, HTMX, testes e segurança. Se a sua base HTTP ainda não está pronta, comece pelo [tutorial de API REST em Go](/aprenda/api-rest-go/) e pelo guia de [middleware com net/http, Chi e Gin](/blog/middleware-go-net-http-chi-gin-producao/).

## O que é templ — e o que ele resolve

O pacote padrão `html/template` interpreta arquivos de template em tempo de execução. Ele é seguro, maduro e suficiente para muitos produtos. O templ segue outra estratégia: você escreve uma sintaxe próxima de HTML em arquivos `.templ`, executa um gerador e recebe arquivos `.go` que entram na compilação normal.

Isso muda a experiência de desenvolvimento em três pontos:

1. **Parâmetros têm tipos Go.** Um componente pode receber `User`, `[]Job` ou `error`, e o compilador verifica as chamadas.
2. **Componentes são funções.** Cabeçalho, card, tabela, alerta e layout podem ser combinados sem depender de herança de templates.
3. **Boa parte dos erros aparece no build.** Uma chamada incompatível ou expressão Go inválida falha antes do deploy.

O templ não é um framework web completo. Ele não escolhe router, banco, autenticação, cache ou estratégia de deploy. Ele resolve a camada de **renderização HTML no servidor**. Essa separação combina com a filosofia do [Clean Architecture em Go sem overengineering](/blog/clean-architecture-go-sem-overengineering/): handlers pequenos, domínio independente e componentes focados em apresentação.

## Instalação e primeiro componente

Instale a CLI e adicione o módulo ao projeto:

```bash
go install github.com/a-h/templ/cmd/templ@latest
go get github.com/a-h/templ
```

Crie `views/hello.templ`:

```templ
package views

templ Hello(name string) {
    <!doctype html>
    <html lang="pt-BR">
        <head>
            <meta charset="utf-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1"/>
            <title>Olá com templ</title>
        </head>
        <body>
            <main>
                <h1>Olá, { name }!</h1>
                <p>Esta página foi renderizada pelo servidor Go.</p>
            </main>
        </body>
    </html>
}
```

Gere o código:

```bash
templ generate
```

O comando cria um arquivo Go ao lado do `.templ`. Esse arquivo gerado deve ser tratado como artefato: não edite na mão. O código-fonte é o arquivo `.templ`; sempre que ele mudar, rode `templ generate` novamente. No CI, uma boa prática é gerar e depois verificar se o repositório ficou limpo, evitando deploy com código gerado desatualizado.

## Renderizando com net/http

Todo componente implementa uma interface com método `Render(context.Context, io.Writer) error`. Por isso, a integração com `net/http` é pequena:

```go
package main

import (
    "log"
    "net/http"

    "example.com/site/views"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")

    if err := views.Hello("Gopher").Render(r.Context(), w); err != nil {
        http.Error(w, "erro ao renderizar página", http.StatusInternalServerError)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /", home)

    log.Fatal(http.ListenAndServe(":8080", mux))
}
```

Em uma aplicação de produção, evite chamar `http.Error` depois que parte do HTML já foi escrita, porque o status `200` pode ter sido enviado. Uma alternativa é renderizar primeiro em um buffer e só copiar para o `ResponseWriter` se não houver erro:

```go
func render(w http.ResponseWriter, r *http.Request, c templ.Component) {
    var buf bytes.Buffer
    if err := c.Render(r.Context(), &buf); err != nil {
        slog.ErrorContext(r.Context(), "falha ao renderizar", "err", err)
        http.Error(w, "erro interno", http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    _, _ = buf.WriteTo(w)
}
```

Esse helper se encaixa naturalmente em uma chain com [recovery, request ID e logging estruturado](/blog/middleware-go-net-http-chi-gin-producao/). Para erros de produção, use também o padrão do [slog em Go](/blog/slog-go-logging-estruturado/), sem expor detalhes internos para o navegador.

## Componentes, propriedades e composição

O ganho real aparece quando a página deixa de ser um bloco único. Considere um portal de vagas com um tipo de domínio:

```go
package model

type Job struct {
    Title    string
    Company  string
    Location string
    Remote   bool
}
```

O componente recebe esse tipo diretamente:

```templ
package views

import "example.com/site/model"

templ JobCard(job model.Job) {
    <article class="job-card">
        <h2>{ job.Title }</h2>
        <p>{ job.Company } — { job.Location }</p>
        if job.Remote {
            <span class="pill remote">Remoto</span>
        }
    </article>
}

templ JobList(jobs []model.Job) {
    <section aria-label="Vagas encontradas">
        if len(jobs) == 0 {
            <p>Nenhuma vaga encontrada com estes filtros.</p>
        } else {
            for _, job := range jobs {
                @JobCard(job)
            }
        }
    </section>
}
```

A chamada `@JobCard(job)` compõe um componente dentro de outro. Se `JobCard` mudar para receber outro tipo, os locais incompatíveis falham na compilação. Compare isso com um template baseado em `map[string]any`: um campo renomeado pode virar um valor vazio percebido apenas depois que a página estiver no ar.

Não passe o banco ou serviços para os componentes. Busque e prepare os dados no handler ou na camada de aplicação, depois entregue uma view model pequena. Isso facilita testes e impede que a camada de HTML controle transações, timeouts ou regras de negócio.

## Layout compartilhado sem duplicação

Um layout pode receber o título e um bloco filho:

```templ
package views

templ Layout(title string) {
    <!doctype html>
    <html lang="pt-BR">
        <head>
            <meta charset="utf-8"/>
            <meta name="viewport" content="width=device-width, initial-scale=1"/>
            <title>{ title }</title>
        </head>
        <body>
            <header>
                <a href="/">Minha aplicação</a>
            </header>
            <main>
                { children... }
            </main>
        </body>
    </html>
}

templ Home(jobs []model.Job) {
    @Layout("Vagas de Go") {
        <h1>Vagas de Go</h1>
        @JobList(jobs)
    }
}
```

Esse padrão resolve o equivalente a `base.html`, `block` e `define` de outros sistemas, mas com composição explícita. Também permite criar layouts separados para área pública, painel autenticado e e-mails, sem um arquivo global cheio de condicionais.

## templ com HTMX: HTML parcial em vez de API JSON

HTMX adiciona atributos ao HTML para fazer requisições e substituir fragmentos da página. Ele combina bem com templ porque o servidor já sabe renderizar componentes. Em vez de manter um componente React e uma resposta JSON com formatos sincronizados, você devolve o próprio HTML final.

Um formulário de busca pode ficar assim:

```templ
templ SearchForm() {
    <form
        hx-get="/vagas/buscar"
        hx-target="#resultados"
        hx-trigger="submit, input changed delay:400ms from:#q"
        hx-push-url="true">
        <label for="q">Buscar vaga</label>
        <input id="q" name="q" type="search" autocomplete="off"/>
        <button type="submit">Buscar</button>
    </form>
    <div id="resultados"></div>
}
```

A rota devolve apenas a lista:

```go
func searchJobs(repo JobRepository) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        query := strings.TrimSpace(r.URL.Query().Get("q"))

        jobs, err := repo.Search(r.Context(), query)
        if err != nil {
            http.Error(w, "não foi possível buscar", http.StatusInternalServerError)
            return
        }

        render(w, r, views.JobList(jobs))
    }
}
```

Use HTMX quando a interação é naturalmente servidor-cêntrica: filtros, paginação, formulários, modais simples, atualização de status e tabelas. Para editor visual complexo, canvas, colaboração em tempo real ou estado offline sofisticado, um frontend dedicado ainda pode ser a melhor escolha.

HTMX não remove preocupações HTTP. Você continua precisando de [CSRF, autenticação e autorização](/blog/autenticacao-autorizacao-go-apis/), timeout, validação e respostas corretas. O benefício é reduzir a duplicação entre a representação JSON e a interface que a consome.

## Formulários, validação e mensagens de erro

A validação deve acontecer no servidor, mesmo que exista validação no navegador. Uma abordagem limpa é criar uma view model que guarda valores enviados e erros por campo:

```go
type SignupForm struct {
    Name   string
    Email  string
    Errors map[string]string
}
```

O componente usa os dados sem conhecer banco ou serviço:

```templ
templ Signup(form SignupForm) {
    <form method="post" action="/cadastro">
        <label for="name">Nome</label>
        <input id="name" name="name" value={ form.Name }/>
        if message := form.Errors["name"]; message != "" {
            <p role="alert">{ message }</p>
        }

        <label for="email">E-mail</label>
        <input id="email" name="email" type="email" value={ form.Email }/>
        if message := form.Errors["email"]; message != "" {
            <p role="alert">{ message }</p>
        }

        <button type="submit">Criar conta</button>
    </form>
}
```

Ao receber POST inválido, responda `422 Unprocessable Entity` e renderize o formulário preenchido com mensagens claras. Com HTMX, o mesmo fragmento pode substituir o formulário atual. Sem HTMX, você pode renderizar a página inteira ou aplicar Post/Redirect/Get depois de uma submissão bem-sucedida.

## Segurança: escaping ajuda, mas não resolve tudo

Conteúdo dinâmico inserido normalmente por templ é escapado, o que protege contra boa parte dos ataques de cross-site scripting (XSS). Se o nome vier como `<script>alert(1)</script>`, ele deve aparecer como texto, não ser executado.

Ainda assim, revise estas fronteiras:

- **HTML confiável:** não transforme texto de usuário em HTML bruto. Sanitização exige uma política explícita.
- **URLs dinâmicas:** valide esquemas e destinos; não aceite `javascript:` nem redirects arbitrários.
- **Atributos e JavaScript:** evite montar trechos executáveis a partir de entrada externa.
- **CSRF:** formulários autenticados que alteram estado precisam de proteção, independentemente do renderer.
- **Headers:** configure Content Security Policy, `X-Content-Type-Options` e outras proteções adequadas ao produto.
- **Autorização:** esconder um botão no componente não impede acesso à rota. A permissão real é verificada no servidor.

A regra é a mesma do [guia de segurança em aplicações Go](/tutoriais/go-security/): escaping é uma camada, não uma licença para confiar na entrada.

## Testando componentes templ

Como o componente escreve em um `io.Writer`, você pode testá-lo sem iniciar servidor:

```go
func TestJobCard(t *testing.T) {
    job := model.Job{
        Title:    "Pessoa Desenvolvedora Go",
        Company:  "Empresa Exemplo",
        Location: "Brasil",
        Remote:   true,
    }

    var buf bytes.Buffer
    err := views.JobCard(job).Render(context.Background(), &buf)
    if err != nil {
        t.Fatal(err)
    }

    html := buf.String()
    if !strings.Contains(html, "Pessoa Desenvolvedora Go") {
        t.Fatalf("título ausente: %s", html)
    }
    if !strings.Contains(html, "Remoto") {
        t.Fatalf("selo remoto ausente: %s", html)
    }
}
```

Para componentes pequenos, assertions semânticas são mais estáveis que snapshots gigantes. Verifique texto importante, links, atributos de acessibilidade e estados condicionais. Para fluxos completos, combine `httptest` com testes do handler. O [guia de table-driven tests](/blog/testes-tabela-go-guia-table-driven-tests/) ajuda a cobrir listas vazias, erros e diferentes permissões sem repetir setup.

Inclua estas etapas no CI:

```bash
templ generate
templ fmt .
go test ./...
go vet ./...
```

Se o time versiona arquivos gerados, rode também `git diff --exit-code` após `templ generate`. Assim, um pull request falha quando alguém altera `.templ` e esquece de atualizar o `.go` correspondente.

## templ ou html/template: como escolher

| Critério | templ | html/template |
|---|---|---|
| Dependência externa | Sim | Não, biblioteca padrão |
| Modelo | Componentes gerados | Templates interpretados |
| Tipagem dos parâmetros | Forte, via Go | Dados verificados principalmente em runtime |
| Reutilização de UI | Composição de componentes | `define`, `template` e funções auxiliares |
| Build | Exige geração | Não exige codegen |
| Curva inicial | Sintaxe e ferramenta novas | Familiar e amplamente documentado |
| Melhor encaixe | UI com componentes e HTMX | Páginas tradicionais e projetos simples |

Escolha **html/template** se você tem uma landing page, e-mails simples ou um painel pequeno e quer zero dependências. Escolha **templ** se a interface possui vários componentes, a equipe valoriza erros no build e o produto usa renderização server-side com atualizações parciais.

Não migre apenas porque a ferramenta está em alta. Se os templates atuais são estáveis, testados e fáceis de manter, o retorno pode ser pequeno. Se a equipe sofre com `map[string]any`, nomes de blocos, duplicação de componentes e erros descobertos no navegador, templ provavelmente paga o custo de adoção.

## Estrutura de projeto recomendada

Uma estrutura simples evita misturar apresentação e regra de negócio:

```text
cmd/web/main.go
internal/domain/job.go
internal/application/search_jobs.go
internal/http/handlers/jobs.go
internal/http/middleware/
views/layout.templ
views/jobs.templ
views/components/
static/css/
static/js/
```

`internal/http/handlers` converte request em chamada de aplicação. `internal/application` coordena casos de uso. `views` recebe dados prontos e renderiza. Arquivos estáticos ficam separados e podem ser incorporados no binário com [`go:embed`](/blog/go-embed-embutir-arquivos-binario/). Para encerrar o servidor sem cortar respostas durante deploy, aplique também [graceful shutdown em Go](/blog/graceful-shutdown-go-producao/).

Se o objetivo é portfólio e carreira, uma aplicação server-rendered completa mostra mais maturidade que uma página isolada: autenticação, formulário validado, PostgreSQL, testes, Docker e README com trade-offs. Veja as [ideias de projetos Go para portfólio](/blog/projetos-go-para-portfolio-ideias-primeira-vaga/) e as [vagas de Go no Brasil](/vagas/) para conectar a prática ao que empresas estão pedindo.

## Checklist antes de colocar em produção

- [ ] `templ generate` roda no desenvolvimento e no CI.
- [ ] Mudanças em arquivos gerados são verificadas antes do merge.
- [ ] Componentes recebem view models, não conexões de banco ou serviços.
- [ ] Erros de renderização são logados sem vazar detalhes para o usuário.
- [ ] Conteúdo dinâmico permanece escapado; HTML bruto é exceção revisada.
- [ ] Formulários têm validação no servidor e proteção CSRF quando necessária.
- [ ] Handlers propagam `r.Context()` para banco e serviços.
- [ ] HTMX recebe códigos HTTP e fragmentos coerentes para sucesso e erro.
- [ ] Componentes críticos têm testes de estados vazios, erro e permissão.
- [ ] A aplicação tem logging, recovery, timeout e shutdown gracioso.

## Perguntas frequentes

### O que é templ em Go?

templ é uma linguagem e ferramenta de geração de código para criar componentes HTML type-safe em Go. Arquivos `.templ` são transformados em código Go, compilados junto com a aplicação e renderizados no servidor. Isso permite detectar vários erros no build, reutilizar componentes e trabalhar com tipos Go sem montar HTML por concatenação de strings.

### templ substitui html/template?

Não obrigatoriamente. `html/template` continua sendo excelente, faz parte da biblioteca padrão e atende páginas tradicionais com poucas abstrações. templ é especialmente útil quando a interface cresce em componentes, layouts e estados reutilizáveis. A decisão depende da complexidade da UI, da política de dependências e da preferência do time.

### Preciso de React para usar templ?

Não. templ renderiza HTML no servidor e funciona apenas com `net/http` se a página for simples. Para interações sem recarregar a página inteira, HTMX combina bem com templ porque solicita fragmentos HTML ao servidor. JavaScript pequeno e específico ainda pode ser adicionado quando necessário.

### templ protege contra XSS?

templ escapa conteúdo dinâmico por padrão quando ele é inserido como texto ou atributo. Isso reduz o risco de XSS, mas não elimina a necessidade de validar URLs, restringir conteúdo HTML confiável, configurar headers de segurança e evitar transformar entrada do usuário em HTML não escapado.

### templ funciona com Gin, Chi e net/http?

Sim. Um componente templ expõe `Render(context.Context, io.Writer)`, portanto pode ser renderizado em qualquer handler. Com `net/http` e Chi, a integração é direta. Em Gin, Echo ou outros frameworks, basta usar o contexto da requisição e o `ResponseWriter` oferecido pelo framework.

## Conclusão

templ ocupa um espaço útil no ecossistema Go: interfaces server-rendered com componentes, tipagem e pouca infraestrutura de frontend. Ele não torna `html/template` obsoleto e não substitui um framework JavaScript em todas as situações. Seu valor aparece quando você quer manter a aplicação centrada no servidor, reutilizar UI como funções Go e combinar HTML parcial com HTMX.

Comece pequeno: uma página, um layout e dois componentes. Coloque geração e testes no CI, mantenha regras de negócio fora da view e trate segurança como responsabilidade do sistema inteiro. Se a experiência melhorar sem criar uma cadeia de ferramentas maior do que o produto precisa, templ é uma escolha pragmática — bem alinhada com a cultura de Go.
