Comunicación entre Spring Boot y Angular
La comunicación entre Spring Boot y Angular se establece a través de peticiones HTTP, donde Angular actúa como cliente consumiendo los endpoints REST que expone el backend de Spring Boot. Esta arquitectura separa completamente el frontend del backend, permitiendo que cada aplicación se desarrolle y despliegue de forma independiente.
Configuración del HttpClient en Angular
Angular proporciona el HttpClient para realizar peticiones HTTP de forma reactiva. En Angular 20.3, la configuración se realiza utilizando la nueva API de proveedores en la función bootstrapApplication:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(withInterceptorsFromDi()),
// otros proveedores...
]
}).catch(err => console.error(err));
Creación de servicios para consumir la API
La mejor práctica es crear servicios Angular que encapsulen las llamadas a la API REST de Spring Boot. Utilizamos la función inject() para obtener las dependencias:
// user.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
export interface User {
id?: number;
name: string;
email: string;
}
@Injectable({
providedIn: 'root'
})
export class UserService {
private http = inject(HttpClient);
private readonly API_URL = 'http://localhost:8080/api/users';
getUsers(): Observable<User[]> {
return this.http.get<User[]>(this.API_URL);
}
getUserById(id: number): Observable<User> {
return this.http.get<User>(`${this.API_URL}/${id}`);
}
createUser(user: User): Observable<User> {
return this.http.post<User>(this.API_URL, user);
}
updateUser(id: number, user: User): Observable<User> {
return this.http.put<User>(`${this.API_URL}/${id}`, user);
}
deleteUser(id: number): Observable<void> {
return this.http.delete<void>(`${this.API_URL}/${id}`);
}
}
Consumo de la API desde componentes standalone
En Angular 20.3, los componentes standalone son la forma recomendada de crear componentes. Aquí mostramos cómo consumir el servicio:
// user-list.component.ts
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { UserService, User } from '../services/user.service';
@Component({
selector: 'app-user-list',
standalone: true,
imports: [CommonModule],
template: `
<div class="user-list">
<h2>Lista de Usuarios</h2>
<button (click)="loadUsers()" class="btn-refresh">Actualizar</button>
<div *ngIf="loading" class="loading">Cargando...</div>
<div *ngIf="error" class="error">{{ error }}</div>
<ul *ngIf="users.length > 0">
<li *ngFor="let user of users">
<strong>{{ user.name }}</strong> - {{ user.email }}
<button (click)="deleteUser(user.id!)" class="btn-delete">
Eliminar
</button>
</li>
</ul>
</div>
`,
styles: [`
.user-list { padding: 20px; }
.loading { color: #666; }
.error { color: red; }
.btn-refresh, .btn-delete { margin: 5px; padding: 5px 10px; }
.btn-delete { background: #ff4444; color: white; }
`]
})
export class UserListComponent implements OnInit {
private userService = inject(UserService);
users: User[] = [];
loading = false;
error: string | null = null;
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.error = null;
this.userService.getUsers().subscribe({
next: (users) => {
this.users = users;
this.loading = false;
},
error: (err) => {
this.error = 'Error al cargar usuarios: ' + err.message;
this.loading = false;
}
});
}
deleteUser(id: number) {
if (confirm('¿Estás seguro de eliminar este usuario?')) {
this.userService.deleteUser(id).subscribe({
next: () => {
this.users = this.users.filter(u => u.id !== id);
},
error: (err) => {
this.error = 'Error al eliminar usuario: ' + err.message;
}
});
}
}
}
Formularios reactivos para operaciones POST y PUT
Para crear y editar usuarios, utilizamos formularios reactivos con Angular:
// user-form.component.ts
import { Component, inject, Input } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { UserService, User } from '../services/user.service';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [CommonModule, ReactiveFormsModule],
template: `
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<h3>{{ isEdit ? 'Editar Usuario' : 'Crear Usuario' }}</h3>
<div class="form-group">
<label>Nombre:</label>
<input type="text" formControlName="name" class="form-control">
<div *ngIf="userForm.get('name')?.errors?.['required']" class="error">
El nombre es requerido
</div>
</div>
<div class="form-group">
<label>Email:</label>
<input type="email" formControlName="email" class="form-control">
<div *ngIf="userForm.get('email')?.errors?.['required']" class="error">
El email es requerido
</div>
<div *ngIf="userForm.get('email')?.errors?.['email']" class="error">
Email inválido
</div>
</div>
<button type="submit" [disabled]="userForm.invalid || saving">
{{ saving ? 'Guardando...' : (isEdit ? 'Actualizar' : 'Crear') }}
</button>
<div *ngIf="message" class="message">{{ message }}</div>
</form>
`,
styles: [`
.form-group { margin-bottom: 15px; }
.form-control { width: 100%; padding: 8px; }
.error { color: red; font-size: 12px; }
.message { margin-top: 10px; color: green; }
`]
})
export class UserFormComponent {
private fb = inject(FormBuilder);
private userService = inject(UserService);
@Input() user: User | null = null;
@Input() isEdit = false;
saving = false;
message = '';
userForm = this.fb.group({
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]]
});
ngOnInit() {
if (this.user) {
this.userForm.patchValue(this.user);
}
}
onSubmit() {
if (this.userForm.valid) {
this.saving = true;
this.message = '';
const userData: User = this.userForm.value as User;
const operation = this.isEdit && this.user?.id
? this.userService.updateUser(this.user.id, userData)
: this.userService.createUser(userData);
operation.subscribe({
next: (result) => {
this.message = `Usuario ${this.isEdit ? 'actualizado' : 'creado'} correctamente`;
this.saving = false;
if (!this.isEdit) {
this.userForm.reset();
}
},
error: (err) => {
this.message = 'Error al guardar: ' + err.message;
this.saving = false;
}
});
}
}
}
Manejo de errores HTTP
Angular permite manejar errores de forma centralizada usando interceptores HTTP:
// http-error.interceptor.ts
import { HttpErrorResponse, HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
let errorMessage = 'Error desconocido';
if (error.error instanceof ErrorEvent) {
// Error del lado del cliente
errorMessage = `Error: ${error.error.message}`;
} else {
// Error del lado del servidor
switch (error.status) {
case 400:
errorMessage = 'Solicitud incorrecta';
break;
case 401:
errorMessage = 'No autorizado';
break;
case 403:
errorMessage = 'Acceso denegado';
break;
case 404:
errorMessage = 'Recurso no encontrado';
break;
case 500:
errorMessage = 'Error interno del servidor';
break;
default:
errorMessage = `Error ${error.status}: ${error.message}`;
}
}
console.error('HTTP Error:', errorMessage);
return throwError(() => new Error(errorMessage));
})
);
};
Para registrar el interceptor en Angular 20.3:
// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { httpErrorInterceptor } from './app/interceptors/http-error.interceptor';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, {
providers: [
provideHttpClient(
withInterceptors([httpErrorInterceptor])
),
]
});
Parámetros de consulta y headers personalizados
Para enviar parámetros de consulta o headers personalizados en las peticiones:
// user.service.ts (métodos adicionales)
export class UserService {
private http = inject(HttpClient);
private readonly API_URL = 'http://localhost:8080/api/users';
searchUsers(query: string, page: number = 0, size: number = 10): Observable<any> {
const params = new HttpParams()
.set('q', query)
.set('page', page.toString())
.set('size', size.toString());
return this.http.get(`${this.API_URL}/search`, { params });
}
uploadUserData(data: any): Observable<any> {
const headers = new HttpHeaders()
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
return this.http.post(`${this.API_URL}/bulk`, data, { headers });
}
}
Esta configuración establece una comunicación robusta entre Angular y Spring Boot, aprovechando las características modernas de Angular 20.3 como los componentes standalone, la función inject() y los interceptores funcionales, mientras mantiene un código limpio y mantenible.
Configuración de CORS y despliegue conjunto
Configuración de CORS en Spring Boot
CORS (Cross-Origin Resource Sharing) es un mecanismo de seguridad que permite a las aplicaciones web realizar peticiones desde un dominio diferente al que sirve la aplicación. Cuando Angular se ejecuta en http://localhost:4200 y Spring Boot en http://localhost:8080, es necesario configurar CORS para permitir estas peticiones cross-origin.
Configuración CORS a nivel de controlador
La forma más sencilla de habilitar CORS es usando la anotación @CrossOrigin directamente en los controladores:
@RestController
@RequestMapping("/api/users")
@CrossOrigin(origins = "http://localhost:4200")
public class UserController {
@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
// implementación
return ResponseEntity.ok(users);
}
@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
// implementación
return ResponseEntity.ok(savedUser);
}
}
Para múltiples orígenes o configuración más flexible:
@CrossOrigin(
origins = {"http://localhost:4200", "http://localhost:3000"},
methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE},
allowedHeaders = "*",
allowCredentials = "true"
)
@RestController
@RequestMapping("/api/users")
public class UserController {
// métodos del controlador
}
Configuración CORS global
Para una configuración centralizada que aplique a toda la aplicación, creamos una clase de configuración:
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("http://localhost:4200", "http://localhost:3000")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
Configuración CORS con diferentes perfiles
Para manejar diferentes entornos (desarrollo, producción), utilizamos perfiles de Spring:
@Configuration
public class CorsConfig {
@Bean
@Profile("development")
public CorsConfigurationSource corsConfigurationSourceDev() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("http://localhost:*"));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
@Bean
@Profile("production")
public CorsConfigurationSource corsConfigurationSourceProd() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://miapp.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
La configuración correspondiente en application.yml:
spring:
profiles:
active: development
---
spring:
config:
activate:
on-profile: development
web:
cors:
allowed-origins: "http://localhost:4200"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
---
spring:
config:
activate:
on-profile: production
web:
cors:
allowed-origins: "https://miapp.com"
allowed-methods: "GET,POST,PUT,DELETE"
allowed-headers: "*"
allow-credentials: false
Despliegue conjunto: servir Angular desde Spring Boot
Una estrategia común es servir los archivos estáticos de Angular directamente desde Spring Boot. Primero, construimos la aplicación Angular:
# En el directorio del proyecto Angular
ng build --configuration production
Esto genera los archivos optimizados en la carpeta dist/. Copiamos estos archivos a la carpeta src/main/resources/static de Spring Boot:
# Copiar archivos build de Angular a Spring Boot
cp -r dist/mi-app-angular/* src/main/resources/static/
Configuración para servir archivos estáticos y manejar el enrutamiento de Angular:
@Configuration
public class StaticResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// Servir archivos estáticos de Angular
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.setCachePeriod(31556926);
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// Redirigir todas las rutas no-API a index.html para Angular routing
registry.addViewController("/{spring:\\w+}")
.setViewName("forward:/");
registry.addViewController("/**/{spring:\\w+}")
.setViewName("forward:/");
registry.addViewController("/{spring:\\w+}/**{spring:?!(\\.js|\\.css)$}")
.setViewName("forward:/");
}
}
Automatización del build con Maven
Para automatizar el proceso de construcción de Angular dentro del build de Spring Boot, agregamos el plugin frontend-maven-plugin:
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<execution>
<id>install-node-and-npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<configuration>
<nodeVersion>v20.10.0</nodeVersion>
<npmVersion>10.2.3</npmVersion>
</configuration>
</execution>
<execution>
<id>npm-install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>npm-build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<workingDirectory>src/main/frontend</workingDirectory>
</configuration>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.1</version>
<executions>
<execution>
<id>copy-frontend-resources</id>
<phase>process-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes/static</outputDirectory>
<resources>
<resource>
<directory>src/main/frontend/dist/mi-app-angular</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Configuración de proxy para desarrollo
Durante el desarrollo, es útil configurar un proxy en Angular para evitar problemas de CORS. Creamos un archivo proxy.conf.json en el directorio raíz del proyecto Angular:
{
"/api/*": {
"target": "http://localhost:8080",
"secure": true,
"changeOrigin": true,
"logLevel": "debug"
}
}
Modificamos el angular.json para usar el proxy:
{
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"configurations": {
"development": {
"proxyConfig": "proxy.conf.json"
}
},
"defaultConfiguration": "development"
}
}
Ahora podemos ejecutar Angular con el proxy:
ng serve
Con esta configuración, Angular interceptará todas las peticiones a /api/* y las redirigirá a http://localhost:8080, eliminando la necesidad de configurar CORS durante el desarrollo.
Despliegue con Docker
Para despliegue en contenedores, creamos un Dockerfile que construya ambas aplicaciones:
# Dockerfile multi-stage
FROM node:20-alpine AS angular-build
WORKDIR /app
COPY frontend/package*.json ./
RUN npm ci
COPY frontend/ .
RUN npm run build
# Stage 2: Spring Boot
FROM openjdk:21-jdk-slim
WORKDIR /app
COPY target/*.jar app.jar
COPY --from=angular-build /app/dist/mi-app-angular /app/static
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Variables de entorno para configuración dinámica
Para configuración flexible entre entornos, utilizamos variables de entorno:
@Configuration
public class AppConfig {
@Value("${app.frontend.url:http://localhost:4200}")
private String frontendUrl;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(frontendUrl));
configuration.setAllowedMethods(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
En el application.yml:
app:
frontend:
url: ${FRONTEND_URL:http://localhost:4200}
Consideraciones de seguridad en producción
Para entornos de producción, es importante restringir los orígenes CORS y configurar headers de seguridad:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.headers(headers -> headers
.frameOptions().deny()
.contentTypeOptions().and()
.httpStrictTransportSecurity(hstsConfig -> hstsConfig
.maxAgeInSeconds(31536000)
.includeSubdomains(true))
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://midominio.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
}
Esta configuración de CORS y despliegue conjunto permite que las aplicaciones Angular y Spring Boot trabajen seamlessly tanto en desarrollo como en producción, proporcionando flexibilidad y seguridad según el entorno de ejecución.
Fuentes y referencias
Documentación oficial y recursos externos para profundizar en SpringBoot
Documentación oficial de SpringBoot
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, SpringBoot es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de SpringBoot
Explora más contenido relacionado con SpringBoot y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
- Comprender cómo Angular consume APIs REST de Spring Boot mediante HttpClient.
- Configurar servicios y componentes standalone en Angular para interactuar con el backend.
- Implementar manejo de errores HTTP y formularios reactivos en Angular.
- Configurar CORS en Spring Boot para permitir peticiones cross-origin seguras.
- Desplegar conjuntamente Angular y Spring Boot, incluyendo automatización y uso de Docker.