Criterios de calidad para aplicaciones Angular + Java

Criterios de calidad para aplicaciones Angular + Java

Una aplicación full stack se degrada por motivos distintos en cada capa: en el navegador, por renderizados innecesarios e interfaces inaccesibles; en el servidor, por consultas mal dimensionadas y validaciones ausentes. Este artículo reúne cinco criterios de calidad y, para cada uno, muestra el patrón de referencia en Angular 17+ junto a su equivalente en Java 17+ con Spring Boot 3 y Spring Security 6.

Los fragmentos son patrones de referencia: adapta nombres, rutas y modelos al proyecto. La regla que atraviesa los cinco apartados es la misma: la interfaz ayuda, pero la autenticación, la autorización y la integridad de los datos se deciden en el servidor.

CriterioAngularJava / Spring Boot
SeguridadEscape por defecto y guards de rutasAutorización por endpoint y cookies seguras
ArquitecturaEstado centralizado y componentes reutilizablesDominio, servicio y repositorio
RendimientoDetección de cambios eficiente y listas establesConsultas paginadas y concurrencia optimista
CalidadSemántica y anuncio accesible de erroresValidación de entrada y operación transaccional
EscalabilidadCarga diferida de pantallasSesiones compartidas y pools ajustados

1. Seguridad

Proteger datos, identidad y operaciones frente a XSS, filtraciones y accesos indebidos. Angular ayuda en la interfaz, pero la autenticación y la autorización deben validarse siempre en Java. Los secretos viven exclusivamente en el servidor, y las sesiones deben usar cookies seguras con protección CSRF.

Angular: escape por defecto, sesión y protección de rutas

// En la plantilla, la interpolación escapa contenido; no uses bypassSecurityTrustHtml.
<p>{{ comentario }}</p>

@Injectable({ providedIn: 'root' })
export class SessionApi {
  private http = inject(HttpClient);
  me() {
    return this.http.get<Usuario>('/api/me', { withCredentials: true });
  }
}

export const adminGuard: CanActivateFn = () => {
  const auth = inject(AuthService);
  const router = inject(Router);
  return auth.isAdmin() ? true : router.parseUrl('/403');
};

La llamada con withCredentials: true hace que la petición lleve la cookie de sesión, y el guard evita llegar a una ruta que el usuario no puede usar. Ninguno de los dos sustituye a la comprobación del servidor.

Java: autenticación, autorización, sesión y secreto fuera del código

@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
    return http
        .authorizeHttpRequests(a -> a
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .anyRequest().authenticated())
        .formLogin(Customizer.withDefaults()) // CSRF permanece habilitado por defecto
        .build();
}

@Value("${PAYMENT_API_KEY}")
private String paymentApiKey; // Inyectar desde variable/gestor de secretos; nunca exponerla al cliente.

La configuración de la cookie de sesión vive en application.properties:

server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=strict

Con http-only el JavaScript no puede leer la cookie, con secure solo viaja por HTTPS y con same-site=strict el navegador no la envía en peticiones cruzadas.

2. Arquitectura

Separar componentes visuales, estado y composición en Angular, y servicios, dominio y persistencia en Java. La interfaz no debe contener reglas de negocio ni acceder directamente a la base de datos. Cada capa expone contratos claros y tiene una responsabilidad definida.

Angular: estado centralizado y componentes reutilizables

@Injectable({ providedIn: 'root' })
export class CartStore {
  readonly items = signal<Producto[]>([]);
  readonly total = computed(() =>
    this.items().reduce((sum, p) => sum + p.precio, 0)
  );
  add(producto: Producto) {
    this.items.update(items => [...items, producto]);
  }
}

Java: dominio, servicio y repositorio

interface OrderRepository extends JpaRepository<Order, UUID> {}

@Service
class OrderService {
    private final OrderRepository orders;
    OrderService(OrderRepository orders) { this.orders = orders; }

    @Transactional
    Order create(Order order) { return orders.save(order); }
}

El store concentra el estado del carrito en un solo lugar, y OrderService es el único punto que decide cómo se crea un pedido. El repositorio persiste: no conoce la interfaz ni las reglas de presentación.

3. Rendimiento

Reducir trabajo innecesario de renderizado, memoria y carga en el cliente; en el servidor, controlar consultas, concurrencia y latencia. Prioriza mediciones, paginación e índices antes de añadir caché. Evita suscripciones sin liberar y consultas que carguen más datos de los necesarios.

Angular: detección eficiente y seguimiento estable de listas

@Component({
  selector: 'app-product-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `@for (p of products(); track p.id) {
    <app-product-card [product]="p" />
  }`
})
export class ProductListComponent {
  products = input.required<Producto[]>();
}

Con OnPush el componente solo se reevalúa cuando cambian sus entradas o su estado, y track p.id evita reconstruir la lista completa cuando solo cambia un elemento.

Java: consulta paginada y control de concurrencia optimista

interface OrderRepository extends JpaRepository<Order, UUID> {
    Page<Order> findByStatus(Status status, Pageable page);
}

// En la entidad Order:
@Version
private Long version; // Detecta actualizaciones concurrentes en conflicto.

// En el servicio:
Page<Order> page = orders.findByStatus(
    Status.PAID, PageRequest.of(0, 50, Sort.by("createdAt").descending())
);

La página pedida limita el tamaño de la respuesta, y @Version obliga a resolver un conflicto cuando dos transacciones escriben sobre el mismo registro. Los dos evitan que el coste crezca sin control.

4. Calidad

La accesibilidad, la experiencia de usuario y la consistencia visual hacen que la interfaz sea usable. En el servidor, las validaciones, las transacciones y las restricciones de datos protegen la integridad; el monitoreo y los controles de salud ayudan a sostener la disponibilidad. La validación del cliente nunca sustituye a la del servidor.

Angular: semántica, etiqueta y anuncio accesible de errores

<label for="email">Correo electrónico</label>
<input id="email" type="email" [formControl]="email"
       required aria-describedby="email-error">
<p id="email-error" aria-live="polite">{{ emailError() }}</p>

El label asociado con for da nombre al campo, aria-describedby enlaza la descripción y aria-live="polite" anuncia el error sin interrumpir al usuario.

Java: validación de entrada y operación transaccional

public record CreateOrder(
    @NotBlank String sku,
    @Positive int quantity
) {}

@PostMapping("/api/orders")
@Transactional
ResponseEntity<Order> create(@Valid @RequestBody CreateOrder request) {
    return ResponseEntity.status(HttpStatus.CREATED).body(orderService.create(request));
}

@Valid activa las restricciones declaradas en el record antes de entrar en el método, y @Transactional hace que la operación se confirme completa o no se confirme en absoluto.

5. Escalabilidad

En Angular, dividir la aplicación y cargar módulos o pantallas bajo demanda reduce el peso inicial y ordena los clientes complejos. En Java, distribuir instancias detrás de un balanceador exige servicios sin estado local o sesiones compartidas. La capacidad se dimensiona con métricas, límites de concurrencia y el presupuesto real de la base de datos.

Angular: carga diferida de una pantalla

export const routes: Routes = [
  {
    path: 'reports',
    loadComponent: () => import('./reports.page')
      .then(m => m.ReportsPage)
  }
];

El chunk de reports solo se solicita cuando alguien entra a esa ruta.

Java y Spring Boot: sesiones compartidas y límites de conexión

# Requiere la dependencia Spring Session Data Redis y Redis externo.
spring.session.store-type=redis
spring.data.redis.host=${REDIS_HOST}

# Ajustar según la capacidad de la base de datos y el número de instancias.
spring.datasource.hikari.maximum-pool-size=20

Con la sesión fuera de la instancia, cualquier nodo detrás del balanceador atiende al mismo usuario, y el tamaño del pool se elige con la capacidad real de la base de datos y el número de instancias en marcha.

Reglas clave

  • Los guards de Angular solo mejoran la navegación; Java debe autorizar cada endpoint.
  • No guardar claves privadas ni secretos en Angular, variables de entorno del cliente ni repositorios.
  • Ajustar cookies, CSRF, CORS, índices, caché y pools según el modelo real de despliegue.
  • Verificar el comportamiento con pruebas de seguridad, accesibilidad, carga y concurrencia, sobre el modelo de despliegue real.

© Arturo Juárez Monroy