José Díaz
Fundador de JoeDayz y JefToken | Arquitecto Senior
Generás el API con Spring Boot 4.1.2 en el 8080 y el front con Angular 22 en el 4200. Un signal recibe el JSON y la tabla lo pinta con @for.
La clase usa Java 25 (Temurin), Node v26.13.0 y npm 11.20.0. El repo es joedayz/pevisa-spring-angular, con dos carpetas: backend y frontend.
java -version
node -v
npm -v
npm install -g @angular/cli
En start.spring.io el proyecto queda así:
| Campo | Valor |
|---|---|
| Build | Maven |
| Lenguaje | Java |
| Spring Boot | 4.1.2 |
| Group | pe.com.pevisaautoparts |
| Artifact | backend |
| Java | 25 |
Spring Boot trae el servidor embebido. No hace falta desplegar un EAR en Tomcat o Jetty a mano.
El catálogo vive en GET /api/repuestos. El controlador delega en el servicio:
@RestController
@RequestMapping("/api/repuestos")
public class RepuestoController {
private final RepuestoService repuestoService;
public RepuestoController(RepuestoService repuestoService) {
this.repuestoService = repuestoService;
}
@GetMapping
public List<Repuesto> listar() {
return repuestoService.listar();
}
}
Cada repuesto tiene id, codigo, nombre, marca, categoria, stock y precio. Lo levantás con el wrapper de Maven:
./mvnw spring-boot:run
curl http://localhost:8080/api/repuestos | jq
El JSON trae 8 ítems. El primero es FLT-001 (filtro de aceite, Mann). Más abajo aparecen ELE-017 (batería Bosch) y ACE-003 (aceite Castrol 5W-30).
El test no llama al servidor: usa MockMvc.
mockMvc.perform(get("/api/repuestos"))
.andExpect(status().isOk())
.andExpect(jsonPath("$[0].codigo").value("FLT-001"))
.andExpect(jsonPath("$.length()").value(8));
Un id que no existe responde 404:
mockMvc.perform(get("/api/repuestos/999"))
.andExpect(status().isNotFound());
ng new frontend pregunta el sistema de estilos: CSS, Tailwind, Sass o Less. El proyecto de la clase usa CSS. En package.json quedan @angular/core y el resto de paquetes en ^22.2.0, y @angular/cli en ^22.2.5. El package manager fijado es npm@11.20.0.
ng serve
El CLI deja la app en http://localhost:4200. IntelliJ muestra el estado Angular CLI Server.
El componente raíz es standalone, selector app-root. El estado no es un array suelto: es un signal.
protected readonly repuestos = signal<Repuesto[]>([]);
protected readonly cargando = signal(true);
protected readonly error = signal<string | null>(null);
El servicio se inyecta con inject y pide el API con HttpClient:
private readonly http = inject(HttpClient);
private readonly apiUrl = 'http://localhost:8080/api/repuestos';
listar(): Observable<Repuesto[]> {
return this.http.get<Repuesto[]>(this.apiUrl);
}
El constructor se suscribe y escribe los signals. Si la llamada falla, el mensaje es Error al cargar los repuestos y cargando pasa a false.
La página se titula Catálogo de repuestos y muestra 8 repuestos en catálogo. Las columnas son Repuesto, Marca, Categoría, Stock y Precio. El cuerpo no usa *ngFor: usa @for y trackea por id.
@for (repuesto of repuestos(); track repuesto.id) {
<tr>
<td class="codigo">{{ repuesto.codigo }}</td>
<td>{{ repuesto.nombre }}</td>
<td>{{ repuesto.marca }}</td>
<td>{{ repuesto.categoria }}</td>
</tr>
}
@for exige declarar NgForOf en imports del componente. Para el precio se suma CurrencyPipe al mismo arreglo.
El navegador está en el 4200 y el API en el 8080. Sin CORS el HttpClient no lee la respuesta. WebConfig abre solo el origen del front y solo GET:
registry.addMapping("/**")
.allowedOrigins("http://localhost:4200")
.allowedMethods("GET");
En DevTools, la petición repuestos vuelve con los 8 ítems y la tabla los pinta.
Si en la plantilla escribís {{ error }}, el compilador avisa que error es una función y hay que invocarla. El diagnóstico es NG8109 y la línea es la del estado de error:
<p class="estado error">{{ error() }}</p>
La referencia de ese aviso está en https://v22.angular.dev/extended-diagnostics/NG8109.
El front queda en el repo con el commit listado de repuestos.
Completa el curso y obtén tu certificado
1 actividades restantes