Neste tutorial, criaremos um aplicativo de bate-papo simples com IA usando Spring AI no backend e Angular no frontend. Este é um ótimo ponto de partida para quem busca integrar recursos de IA em seus aplicativos web.
- O que construiremos
- Pré-requisitos
- Estrutura do Projeto
- Configurando o Spring AI Backend
- Criando o Frontend Angular
- Etapa 1: adicionar material angular ao projeto
- Etapa 2: Crie o serviço de bate-papo
- Etapa 3: gerar o componente de bate-papo
- Etapa 4: Crie o modelo de bate-papo
- Etapa 5: estilize a interface de bate-papo
- Etapa 6: Atualizar o componente do aplicativo
- Etapa 7: Rotas e configuração principal
- Etapa 8: Proxy para a API
- Testando o aplicativo
- Conclusão
O que construiremos
Criaremos uma interface de bate-papo simples onde os usuários podem:
- Enviar mensagens para um assistente de IA
- Receba respostas inteligentes fornecidas pela Gemini

A aplicação consistirá em:
- Um backend Spring Boot com integração Spring AI
- Um frontend Angular com uma interface de chat responsiva
- Comunicação em tempo real entre frontend e backend
Pré-requisitos
Antes de começar, certifique-se de ter:
- Java 21 ou superior
- Node.js e npm
- CLI Angular (
npm install -g @angular/cli) - Um ID de Projeto Gemini (veja como criar um aqui Gemini )
- Seu IDE favorito (usarei VS Code e IntelliJ )
💡 Ganhe 3 meses grátis do IntelliJ Ultimate com o cupom:
LoianeGroner.
Estrutura do Projeto
Veja como nosso projeto está organizado:
spring-ai-angular/
├── api-ai/ # Spring Boot backend
│ └── src/main/java/com/loiane/api_ai/
│ └── chat/
│ ├── SimpleChatService.java
│ ├── ChatController.java
│ └── ChatResponse.java
└── angular-ai/ # Angular frontend
└── src/app/
└── chat/
├── chat-service.ts
├── chat-response.ts
└── simple-chat/
├── simple-chat.ts
├── simple-chat.html
└── simple-chat.scss
Criaremos um projeto Spring Boot e um projeto Angular CLI e colocaremos ambos na pasta spring-ai-angular
Configurando o Spring AI Backend
Crie um novo projeto Spring Boot usando o Spring Initializr ou seu IDE.
Seleções:
- Projeto: Maven
- Linguagem: Java
- Spring Boot: 3.5.3 (selecione a versão mais recente)
- Insira os metadados do seu projeto (nome do pacote, nome do artefato) conforme desejado
- Java: 24 (ou mais recente disponível)
Dependências:
Spring Web: Para criar endpoints REST.Vertex AI Gemini: Para integração com os modelos de linguagem do Google.
Ou selecione a dependência de IA de sua preferência.
Verifique o arquivo pom.xml
Certifique-se pom.xmlde incluir as dependências necessárias para a integração do Google Gemini:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vertex-ai-gemini-spring-boot-starter</artifactId>
</dependency>
</dependencies>
A dependência spring-ai-vertex-ai-gemini-spring-boot-starterfornece os componentes necessários e a autoconfiguração para usar a API do Gemini em um aplicativo Spring Boot, semelhante ao que spring-ai-openai-spring-boot-starteré fornecido pelo OpenAI.
Etapa 1: Configure seu ambiente
Ao usar a IA do Google em um projeto Spring AI, precisamos configurar duas propriedades no application.propertiesarquivo (ou yaml):
spring.ai.vertex.ai.gemini.projectId=${GEMINI_PROJECT_ID}
spring.ai.vertex.ai.gemini.location=us-east4
Embora o ID do projeto possa ser compartilhado, é uma boa prática também passar esse valor como uma variável de ambiente caso você esteja usando projetos diferentes para ambientes diferentes (DEV, QA, PROD).
Etapa 2: Crie o serviço de bate-papo
Vamos criar nosso SimpleChatServiceque lida com as interações da IA:
// filepath: api-ai/src/main/java/com/loiane/api_ai/chat/SimpleChatService.java
package com.loiane.api_ai.chat;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;
@Service
public class SimpleChatService {
private final ChatClient chatClient;
public SimpleChatService(ChatClient.Builder chatClientBuilder) {
this.chatClient = chatClientBuilder.build();
}
public String chat(String message) {
return chatClient.prompt()
.user(message)
.call()
.content();
}
}
Onde:
- Oferece
ChatClientuma API para comunicação com um modelo de IA. Suporta modelos de programação síncrona e de streaming. - O
ChatClienté criado usando umChatClient.Builderobjeto. Você pode obter uma instância autoconfiguradaChatClient.Builderpara qualquer configuração automática do ChatModel Spring Boot ou criar uma programaticamente. - O
chat methodrecebe ummessage. Usaremos oPromptobjeto doChatCliente definiremos a entrada do usuário, passando a mensagem recebida. Ocall()método envia uma solicitação ao modelo de IA econtent()retorna a resposta do modelo de IA como uma String.
E o que está acontecendo aqui?
- Estamos usando o Spring AI
ChatClientpara interagir com o OpenAI. - O serviço é anotado
@Servicepara injeção de dependência. - O
chatmétodo recebe uma mensagem do usuário e retorna a resposta da IA.
Etapa 3: Crie o controlador REST
Agora vamos criar o controlador que expõe nossa funcionalidade de bate-papo:
// filepath: api-ai/src/main/java/com/loiane/api_ai/chat/ChatController.java
package com.loiane.api_ai.chat;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/chat")
public class ChatController {
private final SimpleChatService chatService;
public ChatController(SimpleChatService chatService) {
this.chatService = chatService;
}
@PostMapping
public ChatResponse chat(@RequestBody String message) {
return new ChatResponse(this.simpleChatService.chat(message));
}
}
Pontos principais:
- Estamos usando injeção de construtor para a dependência de serviço como prática recomendada (e não a
@Autowiredanotação). - Este controlador manipula solicitações POST para o
/api/chatendpoint, extraindo a mensagem do corpo da solicitação.
O ChatResponseé um recordpara que possamos formatar a saída:
public record ChatResponse(String message) {}
Etapa 4: Teste via solicitação HTTP
Se você estiver usando o IntelliJ IDEA Ultimate , poderá criar um arquivo api.httpcom o seguinte conteúdo (isso é muito conveniente para testes de solicitações HTTP):
POST http://localhost:8080/api/chat
Content-Type: application/json
{
"message": "Tell me a joke"
}
Como alternativa, você também pode usar o PostMan ou ferramentas semelhantes para simular a solicitação HTTP.
Se enviarmos a solicitação, poderemos obter algo como a seguinte saída:
{
"message": "Why don't scientists trust atoms? \n\nBecause they make up everything! \n \nLet me know if you'd like to hear another one! 😄 \n"
}
Criando o Frontend Angular
Gere um novo projeto Angular usando Angular CLI:
ng new angular-ai --routing
Etapa 1: adicionar material angular ao projeto
Antes de começar a adicionar componentes e serviços, vamos instalar o Angular Material como nossa biblioteca de componentes:
ng add @angular/material
Siga as instruções selecionando seu tema preferido (estou usando Azure and Blue) e se você gostaria de adicionar a tipografia.
Todas as etapas também estão documentadas aqui: https://material.angular.dev/guide/getting-started
Etapa 2: Crie o serviço de bate-papo
Primeiro, vamos criar um serviço para lidar com a comunicação da API:
ng generate service chat/chat-service
Observe que, desde o Angular v20, a convenção de nomenclatura mudou e, em vez de um arquivo chat-service.ts, não estamos sufixando o
servicenome com um hiffen.
// filepath: angular-ai/src/app/chat/chat-service.ts
import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ChatResponse } from './chat-response';
@Injectable({
providedIn: 'root'
})
export class ChatService {
private readonly API = '/api/chat';
private readonly http = inject(HttpClient);
sendChatMessage(message: string): Observable<ChatResponse> {
return this.http.post<ChatResponse>(this.API, { message });
}
}
O que está acontecendo aqui?
private readonly API = '/api/chat': Definimos uma constante para o ponto de extremidade da nossa API. O usoreadonlygarante que esse valor não possa ser alterado após a inicialização, o que é uma boa prática para valores de configuração.private readonly http = inject(HttpClient): Esta é a sintaxe moderna de injeção de dependência do Angular usando ainject()função. É uma alternativa à injeção de construtor e é particularmente útil em serviços. A funçãoHttpClienté injetada para lidar com solicitações HTTP.sendChatMessage(message: string): Observable<ChatResponse>: Este método recebe uma mensagem do usuário como parâmetro de string e retorna umObservable<ChatResponse>. O Observable nos permite lidar com respostas HTTP assíncronas de forma reativa.return this.http.post<ChatResponse>(this.API, { message }): Fazemos uma solicitação POST para o nosso backend Spring Boot. O tipo<ChatResponse>informa ao TypeScript qual tipo de resposta esperar. O segundo parâmetro{ message }cria um objeto JSON com a mensagem do usuário que corresponde ao que o nosso controlador Spring Boot espera.
Pontos principais:
- Estamos usando
inject()a função moderna do Angular para injeção de dependência em vez de injeção de construtor. - O serviço gerencia a comunicação HTTP com nosso backend Spring Boot.
- O método retorna um Observable, pois, neste momento,
httpResourcenão é recomendado para requisições POST, então continuamos usandoHttpClient. - A
{ message }sintaxe cria uma carga JSON que nosso Spring Boot@RequestBodypode desserializar.
Em seguida, vamos criar a ChatResponseinterface que corresponde ao ChatResponseregistro. Use o seguinte comando para criar o arquivo:
ng generate interface chat/chat-response
Com o seguinte conteúdo:
// filepath: angular-ai/src/app/chat/chat-response.ts
export interface ChatMessage {
message: string;
}
Etapa 3: gerar o componente de bate-papo
Vamos criar nosso componente de bate-papo simples:
ng generate component chat/simple-chat
Confira o novo estilo Angular v2025+: https://angular.dev/style-guide . Observe que o nome não é mais
simple-chat.componentts, massimple-chat.tssim uma convenção de nomenclatura mais clara!
Agora vamos implementar nosso componente de bate-papo:
// filepath: angular-ai/src/app/chat/simple-chat/simple-chat.ts
import { Component, effect, ElementRef, inject, signal, viewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatToolbar } from '@angular/material/toolbar';
import { catchError, of } from 'rxjs';
import { ChatResponse } from '../chat-response';
import { ChatService } from '../chat-service';
@Component({
selector: 'app-simple-chat',
imports: [MatCardModule, MatInputModule, MatButtonModule, FormsModule, MatToolbar, MatIconModule],
templateUrl: './simple-chat.html',
styleUrl: './simple-chat.scss'
})
export class SimpleChat {
private readonly chatHistory = viewChild.required<ElementRef>('chatHistory');
private readonly chatService = inject(ChatService);
private readonly local = true;
userInput = '';
isLoading = false;
messages = signal<ChatResponse[]>([
{ message: 'Hello, how can I help you today?', isBot: true },
]);
// Effect to auto-scroll when messages change
private readonly autoScrollEffect = effect(() => {
this.messages(); // Read the signal to track changes
setTimeout(() => this.scrollToBottom(), 0); // Use setTimeout to ensure DOM is updated
});
sendMessage(): void {
this.trimUserMessage();
if (this.userInput !== '' && !this.isLoading) {
this.updateMessages(this.userInput);
this.isLoading = true;
if (this.local) {
this.simulateResponse();
} else {
this.sendChatMessage();
}
}
}
private trimUserMessage() {
this.userInput = this.userInput.trim();
}
private updateMessages(message: string, isBot = false) {
this.messages.update(messages => [...messages, { message, isBot }]);
}
private getResponse() {
setTimeout(() => {
const response = 'This is a simulated response from the AI model.';
this.updateMessages(response, true);
this.isLoading = false;
}, 2000);
}
private simulateResponse() {
this.getResponse();
this.userInput = '';
}
private scrollToBottom(): void {
try {
const chatElement = this.chatHistory();
if (chatElement?.nativeElement) {
chatElement.nativeElement.scrollTop = chatElement.nativeElement.scrollHeight;
}
} catch (err) {
console.error('Failed to scroll chat history:', err);
}
}
private sendChatMessage() {
this.chatService.sendChatMessage(this.userInput)
.pipe(
catchError(() => {
this.updateMessages('Sorry, I am unable to process your request at the moment.', true);
this.isLoading = false;
return of();
})
)
.subscribe((response: ChatResponse) => {
if (response) {
this.updateMessages(response.message, true);
}
this.userInput = '';
this.isLoading = false;
});
}
}
O que está acontecendo aqui?
private readonly chatHistory = viewChild.required<ElementRef>('chatHistory'): Isso usa o sinal moderno do AngularviewChildpara obter uma referência ao elemento DOM do histórico de bate-papo. Orequiredmétodo garante que o elemento exista, e usaremos essa referência para a funcionalidade de rolagem automática.private readonly chatService = inject(ChatService): Estamos injetando nosso serviço de chat usandoinject()a função moderna do Angular em vez da injeção de construtor. Esta é a abordagem recomendada em aplicações Angular modernas.private readonly local = true: Este é um sinalizador de alternância que nos permite alternar entre o modo de simulação local (para testes) e chamadas de API reais. Quandotrue, simula respostas; quandofalse, chama o backend real do Spring Boot.messages = signal<ChatResponse[]>([...]): Estamos usando sinais do Angular para gerenciar o estado das nossas mensagens de bate-papo. Os sinais são a nova primitiva reativa do Angular que rastreia alterações automaticamente e atualiza a interface do usuário com eficiência. Inicializamos isso com uma mensagem de boas-vindas do bot.private readonly autoScrollEffect = effect(() => {...}): Isso cria um efeito que rola automaticamente para o final do chat sempre que novas mensagens são adicionadas. Os efeitos no Angular são executados sempre que suas dependências (neste caso, omessagessinal) mudam. Este é um dos meus casos de uso favoritoseffectsem vez deconsole.log.sendMessage(): Este é o método principal que lida com o envio de mensagens. Ele valida a entrada, adiciona a mensagem do usuário ao chat e simula uma resposta ou chama a API real com base nolocalsinalizador.private updateMessages(message: string, isBot = false): Um método utilitário que atualiza o sinal de mensagens criando uma nova matriz com a mensagem adicional. OisBotparâmetro determina se a mensagem é do usuário ou do assistente de IA.private sendChatMessage(): Este método manipula a solicitação HTTP real para o nosso backend Spring Boot. Ele usa operadores RxJS comocatchErrorpara lidar com erros de forma elegante esubscribeprocessar a resposta.
Principais conceitos angulares utilizados:
- Sinais : Para gerenciamento de estado reativo (
messagessinal). - Efeitos : Para efeitos colaterais automáticos, como rolagem (
autoScrollEffect). - ViewChild : Para referências de elementos DOM (
chatHistory). - Injeção de dependência moderna : usando
inject()função. - Componentes autônomos : não há necessidade de declarações NgModule.
- RxJS : Para manipular operações HTTP assíncronas para a chamada POST para a API Spring.
Este componente demonstra padrões Angular modernos e fornece uma interface de bate-papo limpa e reativa que pode funcionar tanto no modo de simulação para testes quanto com o backend real do Spring Boot.
Etapa 4: Crie o modelo de bate-papo
Aqui está o HTML que se conecta ao nosso componente:
<!-- filepath: angular-ai/src/app/chat/simple-chat/simple-chat.html -->
<mat-card class="chat-container">
<div class="chat-header">
<mat-toolbar>
<span>Simple Chat</span>
</mat-toolbar>
</div>
<div class="chat-history" #chatHistory>
<div class="messages">
@for (message of messages(); track message) {
<div class="message">
<div class="message-bubble" [class.user]="!message.isBot">
</div>
</div>
}
@if (isLoading) {
<div class="message">
<div class="message-bubble">
<span class="typing">...</span>
</div>
</div>
}
</div>
</div>
<div class="chat-input">
<mat-form-field class="full-width">
<mat-label>Ask anything</mat-label>
<input matInput [(ngModel)]="userInput" (keyup.enter)="sendMessage()">
@if (userInput) {
<button matSuffix mat-icon-button aria-label="Send" (click)="sendMessage()" [disabled]="isLoading">
<mat-icon>send</mat-icon>
</button>
}
</mat-form-field>
</div>
</mat-card>
O que está acontecendo aqui?
<mat-card class="chat-container">:Estamos usando o componente card do Angular Material como o contêiner principal para nossa interface de bate-papo.<div class="chat-header">com<mat-toolbar>: Cria uma seção de cabeçalho usando o componente de barra de ferramentas do Angular Material.<div class="chat-history" #chatHistory>: Este é o nosso contêiner de mensagens rolável. A#chatHistoryvariável de referência do modelo nos permite acessar este elemento DOM a partir do nosso componente usandoviewChilda funcionalidade de rolagem automática.@for (message of messages(); track message; let system = $even): Esta é a nova sintaxe de fluxo de controle do Angular (introduzida no Angular v17). Estamos iterando sobre nossomessages()sinal, usando atrackcláusula para otimização de desempenho.[class.user]="!message.isBot": Esta é uma vinculação de classe que aplica condicionalmente auserclasse CSS quando a mensagem NÃO é do bot. Isso nos permite estilizar as mensagens do usuário de forma diferente das mensagens do bot (normalmente alinhadas à direita e à esquerda) e de uma forma preferencial em vez deNgClassdiretivas.@if (isLoading): Outro exemplo da nova sintaxe de fluxo de controle do Angular. Isso mostra condicionalmente um indicador de carregamento com pontos de digitação (...) quando a IA está processando uma resposta.<mat-form-field class="full-width">: Componente de campo de formulário do Angular Material que fornece o estilo de entrada e a funcionalidade de rótulo. Afull-widthclasse garante que ocupe o espaço disponível.[(ngModel)]="userInput": Vinculação de dados bidirecional que conecta o campo de entrada àuserInputpropriedade do nosso componente. Quando o usuário digita, ele atualiza a propriedade; quando a propriedade muda, ele atualiza a entrada. Como temos apenas um campo, não há necessidade de formulários complexos aqui.(keyup.enter)="sendMessage()": Vinculação de evento que chama nossosendMessage()método quando o usuário pressiona a tecla Enter. Isso fornece uma maneira conveniente de enviar mensagens sem clicar no botão.@if (userInput): Exibe o botão de envio apenas quando há texto no campo de entrada. Este é um toque agradável de UX que evita o envio de mensagens vazias.[disabled]="isLoading": Vinculação de propriedade que desabilita o botão de envio enquanto uma solicitação está em andamento, impedindo múltiplos envios.
Principais conceitos angulares utilizados:
- Nova sintaxe de fluxo de controle :
@fore@ifem vez de*ngFore*ngIf - Variáveis de referência de modelo :
#chatHistorypara acesso DOM - Ligação de classe :
[class.user]para estilo condicional - Vinculação de eventos :
(click)e(keyup.enter)para interações do usuário - Vinculação de propriedade :
[disabled]para estados de botões dinâmicos - Ligação de dados bidirecional :
[(ngModel)]para entrada de formulário - Componentes de materiais angulares :
mat-card,mat-toolbar,mat-form-field, etc.
Etapa 5: estilize a interface de bate-papo
Agora vamos adicionar algum estilo ao nosso HTML:
// filepath: angular-ai/src/app/chat/simple-chat/simple-chat.component.scss
.chat-container {
display: flex;
flex-direction: column;
height: 80vh;
margin: 36px;
}
.chat-header {
flex: 0 0 auto; /* Header doesn't grow or shrink */
}
.chat-history {
flex: 1 1 auto; /* Chat history takes up available space */
overflow-y: auto; /* Add scrollbar if content overflows */
padding: 10px;
}
.messages {
flex: 1;
margin-bottom: 16px;
overflow-y: auto;
padding-right: 8px;
}
.message {
margin-bottom: 8px;
}
.message-bubble {
padding: 10px;
border-radius: 10px;
max-width: 80%;
display: inline-block;
}
.user {
background-color: #d7e3ff; /* Light blue for user messages */
border-radius: 10px;
align-self: flex-end; /* Align to the right */
margin-left: auto;
}
.chat-input {
flex: 0 0 auto; /* Input bar doesn't grow or shrink */
padding: 10px;
}
.full-width {
width: 100%;
}
.typing {
display: inline-block;
overflow: hidden;
white-space: nowrap;
animation: typing 1s steps(10) infinite alternate; // changed from 2s to 1s
}
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
O que está acontecendo aqui?
.chat-container: Este é o nosso contêiner principal usando CSS Flexbox. Definimos que eleflex-direction: columnempilhe elementos verticalmente, com uma altura fixa de80vh(80% da altura da janela de visualização) e margem consistente. Isso cria a base para o layout do nosso chat..chat-header: Usosflex: 0 0 autoque significam que ele não cresce nem encolhe — mantém seu tamanho natural. Isso garante que nosso cabeçalho permaneça no topo com altura consistente, independentemente das alterações de conteúdo..chat-history: Esta é a área de mensagens rolável. Usamosflex: 1 1 autopara que ocupe todo o espaço disponível entre o cabeçalho e a entrada.overflow-y: autoAdiciona uma barra de rolagem quando as mensagens excedem a altura do contêiner..messages: Estilo adicional para o contêiner de mensagens com espaçamento e preenchimento adequados.padding-right: 8pxConsidera o espaço da barra de rolagem..message: Espaçamento simples entre mensagens individuais usadomargin-bottom: 8pxpara criar separação visual..message-bubble: Isso estiliza cada balão de mensagem com cantos arredondados, preenchimento e uma largura máxima de 80% para evitar que as mensagens ocupem toda a largura. Issodisplay: inline-blockpermite o alinhamento adequado..user: Esta classe é aplicada condicionalmente às mensagens do usuário (lembre-se[class.user]="!message.isBot"do modelo). Ela usa um fundo azul-claromargin-left: autopara empurrar as mensagens do usuário para o lado direito do chat, criando o padrão típico de alinhamento de chat..chat-input: Semelhante ao cabeçalho, este serveflex: 0 0 autopara manter seu tamanho na parte inferior do contêiner. O preenchimento garante o espaçamento adequado das bordas..full-width: Uma classe utilitária que faz com que o campo de entrada ocupe toda a largura disponível dentro de seu contêiner..typing: Isso cria o indicador de digitação animado. Usamosoverflow: hiddenewhite-space: nowrappara controlar a exibição do texto e, em seguida, aplicamos uma animação personalizada que cria o efeito de digitação.@keyframes typing: Isso define a animação que faz com que os pontos de digitação pareçam “digitar”, animando a largura de 0 a 100%. Issosteps(10) infinite alternatecria um efeito de digitação suave e contínua.
Etapa 6: Atualizar o componente do aplicativo
Agora que nossa interface de bate-papo está pronta, vamos voltar ao componente principal e adicionar o código restante.
// filepath: angular-ai/src/app/app.ts
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { RouterLink, RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet, MatToolbarModule, MatButtonModule, MatIconModule, RouterLink],
templateUrl: './app.html',
styleUrl: './app.scss'
})
export class App {
readonly title = 'AI-Spring-Angular';
}
Este componente de aplicativo serve como nosso shell de aplicativo, fornecendo a barra de ferramentas de navegação principal e uma saída de roteador onde nosso componente de bate-papo será exibido.
O app.htmlarquivo:
<mat-toolbar>
<span class="title"></span>
<button mat-button aria-label="Simple Chat" routerLink="/simple-chat">Simple Chat</button>
</mat-toolbar>
<router-outlet></router-outlet>
E finalmente, o arquivo SCSS:
.title {
margin-right: 20px;
}
Etapa 7: Rotas e configuração principal
Se um app.routesarquivo não foi criado durante a criação do projeto, vá em frente e crie um com o seguinte conteúdo:
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'simple-chat', pathMatch: 'full' },
{ path: 'simple-chat',
loadComponent: () => import('./chat/simple-chat/simple-chat').then(c => c.SimpleChat)
},
{ path: '**', redirectTo: 'simple-chat' }
];
O que está acontecendo aqui?
{ path: '', redirectTo: 'simple-chat', pathMatch: 'full' }: Esta é a configuração de rota padrão. Quando os usuários acessam a URL raiz (/), eles serão redirecionados automaticamente para/simple-chat. IssopathMatch: 'full'garante que esse redirecionamento só aconteça quando o caminho estiver exatamente vazio (não apenas quando começar com vazio).{ path: 'simple-chat', loadComponent: () => import('./chat/simple-chat/simple-chat').then(c => c.SimpleChat) }: Isso define nossa rota de bate-papo usando carregamento lento .loadComponent: Usa importações dinâmicas para componentes autônomos.import('./chat/simple-chat/simple-chat'): Importa dinamicamente nosso componente de bate-papo.then(c => c.SimpleChat): Extrai aSimpleChatclasse do módulo importado
{ path: '**', redirectTo: 'simple-chat' }: Esta é a rota curinga que captura quaisquer URLs não correspondentes. Se alguém tentar acessar uma rota inexistente, será redirecionado para o nosso componente de chat. Esta é uma boa prática de UX para lidar com cenários do tipo 404.
E finalmente, vamos revisar o app.config.tsarquivo:
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient()
]
};
O que está acontecendo aqui?
import { provideHttpClient } from '@angular/common/http': Isso importa o provedor de cliente HTTP que precisamos para fazer chamadas de API para nosso backend do Spring Boot.provideZonelessChangeDetection(): Esta é a nova estratégia experimental de detecção de alterações do Angular que não depende do Zone.js. Ela tem melhor desempenho e funciona particularmente bem com sinais, que estamos usando em todo o nosso componente de bate-papo.provideRouter(routes): Isso configura o sistema de roteamento do Angular com nossas definições de rota. Ele pega nossoroutesarray e configura o sistema de navegação para nossa aplicação.provideHttpClient(): Isso configura o cliente HTTP do Angular para fazer solicitações de API. Precisamos disso para nos comunicar com nosso backend do Spring Boot ao enviar mensagens de bate-papo.
Esta configuração representa a abordagem moderna do Angular para a configuração de aplicações. É mais explícita, adaptável a árvores e com melhor desempenho do que a abordagem tradicional do NgModule. provideZonelessChangeDetection()É particularmente interessante, pois funciona perfeitamente com nosso componente de bate-papo baseado em sinais, proporcionando melhor desempenho sem a sobrecarga do Zone.js.
Etapa 8: Proxy para a API
Pessoalmente, não sou muito fã de usar CORS se não for necessário. Para desenvolvimento local, isso definitivamente não é necessário e considero uma boa prática não usar CORS localmente. Por esse motivo, prefiro sempre criar um proxy.conf.jsarquivo na pasta raiz do projeto:
// file path: angular-ai/proxy.conf.js
const PROXY_CONFIG = [
{
context: ["/api"],
target: "http://localhost:8080/",
secure: false,
logLevel: "debug",
},
];
module.exports = PROXY_CONFIG;
O que está acontecendo aqui?
const PROXY_CONFIG = [...]: Isso cria uma matriz de configuração de proxy que informa ao servidor de desenvolvimento do Angular como lidar com solicitações de API durante o desenvolvimento.context: ["/api"]: Especifica quais caminhos de URL devem ser proxy. Qualquer solicitação que comece com/apiserá interceptada e encaminhada para o nosso backend Spring Boot. Isso corresponde ao endpoint que definimos em nossoChatService(private readonly API = '/api/chat').target: "http://localhost:8080/": Este é o destino para onde as solicitações proxy serão encaminhadas. Como nosso aplicativo Spring Boot roda na porta 8080 por padrão, todas as/apisolicitações do nosso aplicativo Angular (rodando na porta 4200) serão enviadas para .http://localhost:8080/apisecure: false: Isso desabilita a verificação do certificado SSL. Como estamos trabalhando com HTTP (não HTTPS) no desenvolvimento local, configuramos essa opçãofalsepara evitar problemas relacionados a SSL.logLevel: "debug": Isso permite o registro detalhado das operações de proxy no console. É útil para depurar problemas de proxy durante o desenvolvimento — você verá exatamente quais solicitações estão sendo processadas por proxy e para onde estão indo.module.exports = PROXY_CONFIG: Isso exporta a configuração para que o Angular CLI possa usá-la ao iniciar o servidor de desenvolvimento.
Por que usar um proxy em vez do CORS?
- Desenvolvimento mais limpo: não há necessidade de configurar cabeçalhos CORS no seu aplicativo Spring Boot para desenvolvimento local.
- Ambiente semelhante ao de produção: seu frontend faz solicitações para a mesma origem, o que é mais semelhante à configuração de produção.
- Segurança: evita a complexidade e potenciais problemas de segurança da configuração do CORS.
- Simplicidade: um único arquivo de configuração lida com todo o roteamento da API.
Para usar este proxy, você precisará iniciar seu servidor de desenvolvimento Angular com ( package.json):
"start": "ng serve --proxy-config proxy.conf.js -o"
Use npm run startem vez de ng servediretamente.
Ou adicione-o ao seu angular.jsonarquivo na configuração de serviço:
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"proxyConfig": "proxy.conf.js"
}
}
Essa configuração garante que, quando seu aplicativo Angular fizer uma solicitação /api/chat, ela seja encaminhada automaticamente para onde nosso backend do Spring Boot está escutando.http://localhost:8080/api/chat
Testando o aplicativo
Etapa 1: iniciar o backend
Navegue até o api-aidiretório e execute:
./mvnw spring-boot:run
O backend deve iniciar na porta 8080.
Etapa 2: iniciar o frontend
No angular-aidiretório, execute:
npm start
O aplicativo Angular iniciará na porta 4200 e será aberto automaticamente no seu navegador ( -oparâmetro que adicionamos ao startscript).
Etapa 3: Teste o bate-papo
- Abra seu navegador e vá para
http://localhost:4200 - Navegue até a seção Bate-papo Simples
- Digite uma mensagem e pressione Enter ou clique em Enviar
- Você deverá ver a resposta da IA aparecer no chat
Dica : Tente fazer perguntas como “O que é Spring AI?” ou “Explique a injeção de dependência no Spring” para testar o conhecimento da IA.

Conclusão
Parabéns! 🎉 Você construiu com sucesso um aplicativo de bate-papo com IA simples usando Spring AI e Angular. Esta base oferece tudo o que você precisa para criar aplicativos mais sofisticados com IA.
Neste tutorial, abordamos:
- Configurando o Spring AI com a integração do Gemini
- Criando uma API RESTful para funcionalidade de chat
- Construindo uma interface de bate-papo Angular responsiva
- Conectando o frontend e o backend
- Lidando com interações do usuário em tempo real
O que vem a seguir?
- Adicione memória de bate-papo para manter o contexto da conversa
- Implementar diferentes modelos ou provedores de IA
- Adicionar autenticação de usuário
- Armazene o histórico de bate-papo em um banco de dados
Boa codificação! 🚀
Quer o código? Acesse o GitHub : https://github.com/loiane/spring-ai-angular




