Ir para o conteúdo
Linguagem para Dominio1788 palavras

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:

  1. 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).
  2. 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:

  1. 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.
  2. Plataformas financeiras e de saúde, que exigem auditoria, transações ACID e compliance rigoroso, coisas que o ecossistema Java resolve com maturidade.
  3. 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

Text
projeto/
  backend/          # Spring Boot (Java 17+)
  frontend/         # React + TypeScript (Vite)
  openapi.yaml      # Contrato compartilhado

Configurando o backend Spring Boot

O backend expõe uma API REST documentada com SpringDoc OpenAPI. A dependência principal no pom.xml:

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:

Bash
npx openapi-typescript http://localhost:8080/v3/api-docs -o ./src/api/schema.ts

Isso 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:

Java
@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:

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):

TYPESCRIPT
// 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:

TYPESCRIPT
// 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