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, 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 e pelo guia de middleware com net/http, Chi e Gin.
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:
- Parâmetros têm tipos Go. Um componente pode receber
User,[]Jobouerror, e o compilador verifica as chamadas. - Componentes são funções. Cabeçalho, card, tabela, alerta e layout podem ser combinados sem depender de herança de templates.
- 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: 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:
go install github.com/a-h/templ/cmd/templ@latest
go get github.com/a-h/templ
Crie views/hello.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:
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:
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:
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. Para erros de produção, use também o padrão do slog em Go, 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:
package model
type Job struct {
Title string
Company string
Location string
Remote bool
}
O componente recebe esse tipo diretamente:
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:
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 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:
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, 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:
type SignupForm struct {
Name string
Email string
Errors map[string]string
}
O componente usa os dados sem conhecer banco ou serviço:
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-Optionse 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: 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:
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 ajuda a cobrir listas vazias, erros e diferentes permissões sem repetir setup.
Inclua estas etapas no CI:
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:
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. Para encerrar o servidor sem cortar respostas durante deploy, aplique também graceful shutdown em Go.
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 e as vagas de Go no Brasil para conectar a prática ao que empresas estão pedindo.
Checklist antes de colocar em produção
-
templ generateroda 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.