Java para Frontend: interface em Java ou React consumindo Spring Boot?
Comece decidindo qual das duas perguntas é a sua
"Java para frontend" descreve duas necessidades que não se parecem na prática:
- Você quer escrever a interface em Java, sem JavaScript no dia a dia. A resposta é um framework que gera a UI a partir de código Java: Vaadin (os componentes rodam no navegador e a lógica fica no servidor), GWT (compila Java para JavaScript) ou Hilla (backend Spring Boot com frontend React tipado, gerado a partir dos endpoints).
- Você tem um backend Java e quer um frontend web moderno (React, Angular, Vue) consumindo essa API com tipos gerados a partir do contrato. Aqui Java continua no servidor; o que muda é como o contrato chega ao TypeScript.
Como decidir em uma linha: se o time é Java e a aplicação é interna (ERP, painel, backoffice), a opção 1 entrega mais rápido; se o produto é público, precisa de SEO, de um design system próprio ou tem time de frontend dedicado, a opção 2 é a escolha.
Este guia cobre as duas, nesta ordem: uma seção curta para quem fica com a primeira, e um exemplo completo com Spring Boot e React com TypeScript para a segunda, que é o cenário mais comum em empresas com sistemas Java já em produção. Applets e JSF com página inteira renderizada no servidor ficam de fora: não são opções para produto novo em 2026.
Por que usar Java junto com o Frontend moderno
Java como motor do backend que alimenta o frontend
A grande força de Java no contexto do frontend não é substituir JavaScript ou TypeScript. É fornecer uma camada de backend tão bem estruturada que o frontend se torna mais simples, previsível e performático.
Com Spring Boot, por exemplo, você consegue criar APIs REST e GraphQL com tipagem forte, validação robusta e documentação automática via OpenAPI. Isso se traduz diretamente em contratos de API que o frontend pode consumir com segurança, gerando tipos TypeScript automaticamente a partir dos schemas do backend.
Existem cenários onde essa combinação se destaca com clareza:
- Aplicações corporativas com regras de negócio complexas, onde o backend Java encapsula toda a lógica e o frontend apenas apresenta dados e coleta inputs.
- Plataformas financeiras e de saúde, que exigem auditoria, transações ACID e compliance rigoroso, coisas que o ecossistema Java resolve com maturidade.
- Sistemas legados que estão migrando para interfaces modernas sem reescrever o core de negócio.
Frameworks Java que atuam diretamente no frontend
Para quem quer escrever Java e obter interfaces web sem tocar em JavaScript, existem opções maduras:
Vaadin: framework full-stack que permite criar UIs web inteiramente em Java. O componente visual roda no browser, mas toda a lógica fica no servidor. É usado em produção por empresas como Nokia e Siemens.
GWT (Google Web Toolkit): compila Java para JavaScript otimizado. Embora menos popular hoje, ainda é a base do Google AdWords e de outros produtos Google.
JHipster: gerador de aplicações que combina Spring Boot no backend com React ou Angular no frontend, criando todo o scaffolding de integração automaticamente.
Hilla (antigo Vaadin Fusion): combina um backend Spring Boot com um frontend em Lit ou React, gerando automaticamente os tipos TypeScript a partir dos endpoints Java.
Setup: configurando a integração Java e TypeScript
Vamos montar um projeto real onde Spring Boot serve como backend e um frontend React com TypeScript consome a API. A geração automática de tipos é o ponto chave.
Estrutura do projeto
projeto/
backend/ # Spring Boot (Java 17+)
frontend/ # React + TypeScript (Vite)
openapi.yaml # Contrato compartilhadoConfigurando o backend Spring Boot
O backend expõe uma API REST documentada com SpringDoc OpenAPI. A dependência principal no pom.xml:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.3.0</version>
</dependency>Com isso, ao rodar o backend, o schema OpenAPI fica disponível em /v3/api-docs, e o Swagger UI em /swagger-ui.html.
Gerando tipos TypeScript a partir do backend Java
Aqui está a mágica da integração. Usando a ferramenta openapi-typescript-codegen ou openapi-typescript, você gera interfaces TypeScript diretamente do schema que o Spring Boot produz:
npx openapi-typescript http://localhost:8080/v3/api-docs -o ./src/api/schema.tsIsso elimina a duplicação de tipos entre backend e frontend. Quando o Java muda, o TypeScript reflete.
Exemplo prático: API de produtos com Spring Boot e React TypeScript
O endpoint Java
Considere um controller Spring Boot simples:
@RestController
@RequestMapping("/api/produtos")
public class ProdutoController {
@GetMapping
public List<ProdutoDTO> listar() {
return produtoService.listarTodos();
}
@PostMapping
public ResponseEntity<ProdutoDTO> criar(@Valid @RequestBody CriarProdutoRequest request) {
ProdutoDTO criado = produtoService.criar(request);
return ResponseEntity.status(HttpStatus.CREATED).body(criado);
}
}O ProdutoDTO em Java:
public record ProdutoDTO(
Long id,
String nome,
BigDecimal preco,
String categoria,
LocalDateTime criadoEm
) {}Consumindo no frontend TypeScript
Após gerar os tipos com o openapi-typescript, o frontend recebe interfaces como esta (geradas automaticamente, mas apresento aqui de forma simplificada para clareza):
// src/api/types.ts (gerado ou baseado no schema OpenAPI)
export interface ProdutoDTO {
id: number;
nome: string;
preco: number;
categoria: string;
criadoEm: string;
}
export interface CriarProdutoRequest {
nome: string;
preco: number;
categoria: string;
}
// src/api/produtoService.ts
const API_BASE = import.meta.env.VITE_API_URL || "http://localhost:8080";
export async function listarProdutos(): Promise<ProdutoDTO[]> {
const response = await fetch(`${API_BASE}/api/produtos`, {
headers: { "Content-Type": "application/json" },
});
if (!response.ok) {
const erro = await response.text();
throw new Error(`Falha ao listar produtos: ${response.status} ${erro}`);
}
return response.json();
}
export async function criarProduto(
dados: CriarProdutoRequest
): Promise<ProdutoDTO> {
const response = await fetch(`${API_BASE}/api/produtos`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(dados),
});
if (!response.ok) {
const erro = await response.text();
throw new Error(`Falha ao criar produto: ${response.status} ${erro}`);
}
return response.json();
}Componente React consumindo a API
Agora o componente que usa o serviço acima com tratamento de estado adequado:
// src/components/ListaProdutos.tsx
import { useEffect, useState } from "react";
import { listarProdutos, criarProduto } from "../api/produtoService";
import type { ProdutoDTO, CriarProdutoRequest } from "../api/types";
interface EstadoLista {
produtos: ProdutoDTO[];
carregando: boolean;
erro: string | null;
}
export function ListaProdutos() {
const [estado, setEstado] = useState<EstadoLista>({
produtos: [],
carregando: true,
erro: null,
});
const [novoProduto, setNovoProduto] = useState<CriarProdutoRequest>({
nome: "",
preco: 0,
categoria: "",
});
useEffect(() => {
carregarProdutos();
}, []);
async function carregarProdutos(): Promise<void> {
try {
setEstado((prev) => ({ ...prev, carregando: true, erro: null }));
const produtos = await listarProdutos();
setEstado({ produtos, carregando: false, erro: null });
} catch (err) {
const mensagem =
err instanceof Error ? err.message : "Erro desconhecido";
setEstado((prev) => ({
...prev,
carregando: false,
erro: mensagem,
}));
}
}
async function handleCriar(): Promise<void> {
try {
const criado = await criarProduto(novoProduto);
setEstado((prev) => ({
...prev,
produtos: [...prev.produtos, criado],
}));
setNovoProduto({ nome: "", preco: 0, categoria: "" });
} catch (err) {
const mensagem =
err instanceof Error ? err.message : "Erro ao criar produto";
setEstado((prev) => ({ ...prev, erro: mensagem }));
}
}
if (estado.carregando) return <p>Carregando produtos...</p>;
if (estado.erro) return <p>Erro: {estado.erro}</p>;
return (
<div>
<h2>Produtos ({estado.produtos.length})</h2>
<div className="form-criar">
<input
type="text"
placeholder="Nome do produto"
value={novoProduto.nome}
onChange={(e) =>
setNovoProduto((prev) => ({ ...prev, nome: e.target.value }))
}
/>
<input
type="number"
placeholder="Preco"
value={novoProduto.preco}
onChange={(e) =>
setNovoProduto((prev) => ({
...prev,
preco: parseFloat(e.target.value) || 0,
}))
}
/>
<button onClick={handleCriar}>Adicionar Produto</button>
</div>
<ul>
{estado.produtos.map((produto) => (
<li key={produto.id}>
{produto.nome}: R$ {produto.preco.toFixed(2)} ({produto.categoria})
</li>
))}
</ul>
</div>
);
}Observe como a tipagem forte do TypeScript, alimentada pelos tipos gerados a partir do Java, cria uma cadeia de segurança de ponta a ponta. Se o backend mudar o nome de um campo no DTO, o frontend quebra em tempo de compilação, não em produção.
Casos de uso reais onde Java com frontend moderno se destaca
Nubank: utiliza Clojure (JVM) no backend com interfaces React no frontend. A JVM é o motor, e o frontend consome APIs bem definidas.
Mercado Livre: combina Java e Kotlin no backend com React no frontend. A escala exige a performance da JVM no processamento de pedidos, enquanto o frontend precisa da agilidade do ecossistema JavaScript.
Plataformas bancárias (Itaú, Bradesco): muitos sistemas core rodam em Java. As interfaces modernas dos apps e internet banking consomem esses serviços por meio de camadas BFF (Backend for Frontend) que traduzem os dados para o formato ideal do frontend.
Ferramentas internas de empresas SaaS: é extremamente comum encontrar dashboards React consumindo APIs Spring Boot, especialmente em empresas que já possuem times Java experientes.
Boas práticas para integração Java e Frontend
Contrato primeiro (contract-first)
Defina o schema OpenAPI antes de implementar backend ou frontend. Isso permite que os dois times trabalhem em paralelo e reduz conflitos na integração.
Geração automática de tipos no CI/CD
Inclua a geração de tipos TypeScript no pipeline. Se o backend muda e o frontend não se adapta, o build deve falhar antes do deploy.
Use o padrão BFF quando necessário
Em aplicações complexas, crie uma camada intermediária (BFF) em Java ou Node.js que adapta os dados do backend para o formato ideal do frontend. Isso evita que o frontend precise lidar com estruturas complexas de domínio. Se você quer entender melhor as mecânicas JavaScript envolvidas nessa camada, vale conferir o post sobre coerção, closures e protótipos.
Valide nos dois lados, mas confie no backend
O frontend valida para UX (feedback rápido ao usuário). O backend valida para segurança (nunca confie em dados do cliente). Com Java e Bean Validation, as regras ficam declarativas e auditáveis.
Tipagem de ponta a ponta
A combinação Java (tipagem forte no backend) com TypeScript (tipagem forte no frontend) é poderosa. Aproveite isso. Se você ainda tem lacunas sobre como TypeScript e JavaScript lidam com tipos, recomendo a leitura sobre lacunas ocultas do JavaScript avançado.
Conclusão: escolha pela necessidade, não pela linguagem
Java não vai substituir TypeScript no browser. Essa não é a proposta. A verdadeira questão é que Java fornece uma fundação de backend tão sólida e bem tipada que o frontend se beneficia diretamente: tipos gerados automaticamente, contratos de API confiáveis, validação robusta e performance previsível sob carga.
Para equipes que já dominam Java, adotar ferramentas como Vaadin ou Hilla pode fazer sentido em cenários corporativos internos. Para a maioria dos projetos web modernos, a combinação Spring Boot no backend com React/TypeScript no frontend é uma arquitetura testada em produção por milhares de empresas, do Nubank ao Mercado Livre.
O segredo está na integração disciplinada: schemas compartilhados, tipos gerados automaticamente e validação em ambos os lados. Com isso, você aproveita o melhor dos dois mundos sem as dores de cabeça da inconsistência entre camadas.
Se você já trabalha com Java e quer expandir para o frontend, comece pelo setup descrito neste guia. Se já trabalha com frontend e precisa integrar com Java, foque na geração automática de tipos. Nos dois casos, a tipagem de ponta a ponta é o investimento que mais retorna em produtividade e confiabilidade.
Guias Relacionados
Explore mais guias técnicos
Mais de 100 guias práticos de integração entre frameworks, ferramentas e plataformas.
Ver todos os guias