Construyendo una aplicación Full-Stack reactiva con Quarkus y Angular 17
GUÍA PRÁCTICAMar.03.2026

Construyendo una aplicación Full-Stack reactiva con Quarkus y Angular 17

Backend: Java, Quarkus, Hibernate ORM con Panache, Base de datos en memoria (H2). Frontend: Angular 17+, TypeScript, RxJS. --- Genera un nuevo proyecto…

Tecnologías que utilizaremos

  • Backend: Java, Quarkus, Hibernate ORM con Panache, Base de datos en memoria (H2).
  • Frontend: Angular 17+, TypeScript, RxJS.

Parte 1: El Backend con Quarkus

1. Creación del proyecto y configuración

Genera un nuevo proyecto Quarkus agregando las dependencias de RESTEasy Jackson, Hibernate ORM con Panache y el driver de H2.

En tu archivo src/main/resources/application.properties, configura la base de datos en memoria:

# Configuración de Base de Datos H2
quarkus.datasource.db-kind=h2
quarkus.datasource.jdbc.url=jdbc:h2:mem:clientes_db
quarkus.hibernate-orm.database.generation=drop-and-create

2. La Entidad y el Repositorio

Creamos nuestra entidad Cliente usando JPA clásico.

package com.miempresa.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "clientes")
public class Cliente {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public Long id;
    public String nombre;
    public String email;
    public String tipoProblema;

    // Genera aquí tus Getters y Setters...
}

Luego, aprovechamos la magia de Panache para crear el repositorio sin escribir consultas SQL:

package com.miempresa.repository;

import com.miempresa.entity.Cliente;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class ClienteRepository implements PanacheRepository<Cliente> {
}

3. La Capa de Servicio

Aquí manejamos la lógica de negocio y las transacciones de base de datos.

package com.miempresa.service;

import com.miempresa.entity.Cliente;
import com.miempresa.repository.ClienteRepository;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.List;

@ApplicationScoped
public class ClienteService {

    @Inject
    ClienteRepository clienteRepository;

    @Transactional
    public Cliente registrarCliente(Cliente cliente) {
        clienteRepository.persist(cliente);
        return cliente;
    }

    public List<Cliente> obtenerTodos() {
        return clienteRepository.listAll();
    }
}

4. El Controlador REST y el Filtro CORS

Para exponer nuestra API al mundo, creamos el Resource.

package com.miempresa.resource;

import com.miempresa.entity.Cliente;
import com.miempresa.service.ClienteService;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/api/v1/clientes")
public class ClienteResource {

    @Inject
    ClienteService clienteService;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response crearCliente(Cliente cliente) {
        Cliente clienteGuardado = clienteService.registrarCliente(cliente);
        return Response.status(Response.Status.CREATED).entity(clienteGuardado).build();
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public Response listarClientes() {
        return Response.ok(clienteService.obtenerTodos()).build();
    }
}

Pro-Tip (Solución de CORS): Para evitar los dolores de cabeza de los bloqueos de CORS entre Angular y Quarkus en desarrollo, creamos un interceptor global:

package com.miempresa.config;

import jakarta.ws.rs.container.*;
import jakarta.ws.rs.ext.Provider;

@Provider
public class CorsFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext) {
        responseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
        responseContext.getHeaders().add("Access-Control-Allow-Credentials", "true");
        responseContext.getHeaders().add("Access-Control-Allow-Headers", "origin, content-type, accept, authorization");
        responseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, HEAD");
    }
}

Levanta tu servidor con ./mvnw compile quarkus:dev ¡y tu backend estará listo!


Parte 2: El Frontend con Angular

1. Habilitar HttpClient

En las nuevas versiones de Angular con Componentes Standalone, configuramos el cliente HTTP en el app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http'; 

export const appConfig: ApplicationConfig = {
  providers: [provideRouter(routes), provideHttpClient()] 
};

2. El Servicio de Comunicación

Creamos una interfaz para el tipado fuerte y el servicio que conectará con Quarkus (cliente.service.ts):

import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Cliente {
  id?: number;
  nombre: string;
  email: string;
  tipoProblema: string;
}

@Injectable({
  providedIn: 'root'
})
export class ClienteService {
  private http = inject(HttpClient);
  private apiUrl = 'http://localhost:8080/api/v1/clientes';

  registrarCliente(cliente: Cliente): Observable<Cliente> {
    return this.http.post<Cliente>(this.apiUrl, cliente);
  }

  listarClientes(): Observable<Cliente[]> {
    return this.http.get<Cliente[]>(this.apiUrl);
  }
}

3. El Componente (TypeScript)

Aquí implementaremos formularios reactivos y un pequeño truco con ChangeDetectorRef para asegurar que nuestra interfaz se actualice al instante de recibir la respuesta de la red.

import { Component, inject, OnInit, ChangeDetectorRef } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Cliente, ClienteService } from '../../services/cliente.service';

@Component({
  selector: 'app-registro-cliente',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './registro-cliente.html'
})
export class RegistroCliente implements OnInit {

  private formbuilder = inject(FormBuilder);
  private clienteService = inject(ClienteService);
  private cdr = inject(ChangeDetectorRef);

  mensajeExito = "";
  mensajeError = "";
  listaClientes: Cliente[] = [];

  clienteForm = this.formbuilder.group({
    nombre: ['', Validators.required],
    email: ['', [Validators.required, Validators.email]],
    tipoProblema: ['', Validators.required]
  });

  ngOnInit(): void {
    this.cargarClientes(); 
  }

  cargarClientes(){
    this.clienteService.listarClientes().subscribe({
      next: (datos: Cliente[]) => {
        this.listaClientes = datos;
        this.cdr.detectChanges(); // Forzamos la actualización de la UI
      },
      error: (error: any) => console.error('Error al listar', error)
    });
  }

  onSubmit(){
    if(this.clienteForm.valid){
      const nuevoCliente = this.clienteForm.value as Cliente;

      this.clienteService.registrarCliente(nuevoCliente).subscribe({
        next: (respuesta: Cliente) => {
          this.mensajeExito = `Cliente ${respuesta.nombre} registrado con el ID: ${respuesta.id}`;
          this.clienteForm.reset(); 
          this.cargarClientes(); 
          this.cdr.detectChanges(); 
        },
        error: (error: any) => console.error('Error de conexión', error)
      });
    }
  }
}

4. La Vista (HTML)

Usaremos el nuevo Control Flow de Angular (@if, @for) que hace el código mucho más legible y rápido que las directivas estructurales antiguas.

<div style="max-width: 500px; margin: 40px auto; font-family: sans-serif;">
  <h2>Atención al Cliente</h2>
  
  @if (mensajeExito) {
    <div style="background: #d4edda; color: #155724; padding: 10px; margin-bottom: 15px; border-radius: 5px;">
      {{ mensajeExito }}
    </div>
  }

  <form [formGroup]="clienteForm" (ngSubmit)="onSubmit()" style="display: flex; flex-direction: column; gap: 15px;">
    <div>
      <label>Nombre completo:</label><br>
      <input type="text" formControlName="nombre" style="width: 100%; padding: 8px;">
    </div>
    <div>
      <label>Email:</label><br>
      <input type="email" formControlName="email" style="width: 100%; padding: 8px;">
    </div>
    <div>
      <label>Tipo de Problema:</label><br>
      <select formControlName="tipoProblema" style="width: 100%; padding: 8px;">
        <option value="" disabled selected>Seleccione un área</option>
        <option value="Facturacion">Facturación y pagos</option>
        <option value="Soporte">Soporte Técnico</option>
        <option value="Hardware">Garantía de Hardware</option>
      </select>
    </div>
    <button type="submit" [disabled]="clienteForm.invalid" style="padding: 10px; background-color: #007bff; color: white; border: none; border-radius: 5px;">
      Registrar e informar
    </button>
  </form>

  <hr style="margin: 30px 0;">

  <h3>Clientes Registrados</h3>
  <div style="border: 1px solid #ccc; padding: 15px; border-radius: 5px;">
    @for (c of listaClientes; track c.id) {
      <div style="padding: 10px; border-bottom: 1px solid #eee;">
        <strong>{{ c.nombre }}</strong> ({{ c.email }}) <br>
        <span style="color: #666; font-size: 0.9em;">Problema: {{ c.tipoProblema }}</span>
      </div>
    } @empty {
      <p style="color: #666;">No hay clientes en la base de datos.</p>
    }
  </div>
</div>

Finalmente, importa y coloca el selector <app-registro-cliente></app-registro-cliente> en tu app.component.html, ejecuta ng serve y tendrás una aplicación empresarial totalmente reactiva.

Escrito por@ARBE

Comentarios (0)