50% OFF Plus
--:--:--
¡Ver!

Integración con Angular

Avanzado
SpringBoot
SpringBoot
Actualizado: 08/10/2025

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 - Autor del tutorial

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.