Saltar a contenido

6. Servicios: lógica reutilizable y acceso a datos

El componente pide; el servicio sabe cómo resolver.

Generar un servicio

ng g s services/student

Genera student.service.ts y student.service.spec.ts. Sin HTML ni CSS: un servicio no representa interfaz.

Separar presentación de lógica y comunicación

Componente — presenta y reacciona Servicio — resuelve y reutiliza
Muestra datos y recibe acciones del usuario Centraliza reglas y operaciones compartidas
Controla estados visuales: cargando, éxito y error Comunica con la API mediante HttpClient
Solicita operaciones sin conocer URLs ni detalles HTTP Devuelve datos tipados mediante Observables
Debe permanecer pequeño y fácil de entender Puede inyectarse y reutilizarse en varios componentes

El servicio

src/app/services/student.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { NewStudent, Student } from '../models/student.model';

@Injectable({ providedIn: 'root' })
export class StudentService {
  private http = inject(HttpClient);

  list(): Observable<Student[]> {
    return this.http.get<Student[]>('/api/students');
  }

  create(student: NewStudent): Observable<Student> {
    return this.http.post<Student>(
      '/api/students', student
    );
  }
}
  • @Injectable({ providedIn: 'root' }): Angular crea una sola instancia para toda la aplicación y la entrega a quien la pida.
  • inject(HttpClient): el servicio pide su propia dependencia. HttpClient debe estar registrado con provideHttpClient() en app.config.ts (ver Proyecto base).
  • get<Student[]> / post<Student>: el genérico indica el modelo que se espera en la respuesta.

El componente

src/app/students/student-list/student-list.component.ts
import { Component, inject } from '@angular/core';
import { Student } from '../../models/student.model';
import { StudentService } from '../../services/student.service';
import { StudentCardComponent } from '../student-card/student-card.component';

@Component({
  selector: 'app-student-list',
  standalone: true,
  imports: [StudentCardComponent],
  template: `
    @for (s of students; track s.id) {
      <app-student-card [student]="s" />
    }
  `
})
export class StudentListComponent {
  private service = inject(StudentService);
  students: Student[] = [];

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

  load(): void {
    this.service.list()
      .subscribe(data => this.students = data);
  }
}

→ El componente delega la operación y se concentra en la experiencia del usuario.

Observables en lo justo

HttpClient no devuelve los datos directamente, sino un Observable: una promesa de que la respuesta llegará más adelante.

sequenceDiagram
    participant C as StudentListComponent
    participant S as StudentService
    participant A as API /api/students
    C->>S: list()
    S-->>C: Observable<Student[]>
    C->>C: subscribe(...)
    Note over C,A: solo ahora sale la petición
    C->>A: GET /api/students
    A-->>C: JSON → data
    C->>C: this.students = data

Sin subscribe no hay petición

this.service.create(student); no envía nada. La petición HTTP sale solo cuando alguien se suscribe.

Inyección de dependencias

El componente no crea el servicio con new: lo pide con inject(). Es el mismo principio de Spring:

Spring Angular
@Service @Injectable({ providedIn: 'root' })
Inyección por constructor / @Autowired inject(StudentService) o constructor
Bean singleton Servicio root singleton

¿Por qué no new StudentService()?

  • Todos los componentes comparten la misma instancia.
  • Angular resuelve la cadena de dependencias (StudentService → HttpClient → …).
  • En las pruebas se puede reemplazar por un servicio falso sin tocar el componente.

Correspondencia con el backend

Angular HTTP Spring (Controller)
http.get<Student[]>('/api/students') GET /api/students @GetMapping
http.get<Student>('/api/students/42') GET /api/students/42 @GetMapping("/{id}")
http.post<Student>('/api/students', body) POST + JSON @PostMapping + @RequestBody
http.put<Student>('/api/students/42', body) PUT + JSON @PutMapping("/{id}")
http.delete<void>('/api/students/42') DELETE @DeleteMapping("/{id}")
¿Y si el frontend y el backend corren en puertos distintos?

En desarrollo Angular corre en localhost:4200 y Spring en localhost:8080. Para que /api/students llegue al backend, se configura un proxy del servidor de desarrollo:

proxy.conf.json
{ "/api": { "target": "http://localhost:8080", "secure": false } }
angular.json → architect.serve.options
"proxyConfig": "proxy.conf.json"

La alternativa es habilitar CORS en Spring para el origen http://localhost:4200.

Idea clave

Si dos componentes necesitan la misma lógica, probablemente esa lógica pertenece a un servicio.

Siguiente: Routing.