Explicación: Servicios http

Sitio: Campus virtual DAW - damiansu
Curso: Desarrollo Web en Entorno Cliente
Libro: Explicación: Servicios http
Imprimido por: Invitado
Día: domingo, 8 de marzo de 2026, 09:04

1. Explicación

Vamos a contruir una aplicación Angular que consume una API REST y realiza un CRUD completo con filtrado y ordenación

2. Crear proyecto

ng new posts-get --standalone --routing --style=css --skip-tests
cd posts-get
ng serve -o

3. Configurar HttpClient

HttpClient se configura una vez, en main.ts.

src/main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, {
  providers: [provideHttpClient()],
}).catch(console.error);

4. Modelo (interfaz)

Tipado fuerte → errores antes de ejecutar.

ng generate interface models/post
ng g i models/post

src/app/models/post.model.ts

export interface Post {
  userId: number;
  id: number;
  title: string;
  body: string;
}

Interfaces para datos, clases para lógica

5. Servicio (GET básico)

El servicio:

  • No tiene HTML

  • Solo devuelve datos

  • No sabe quién los usa

Crear servicio:

ng g s services/posts

src/app/services/posts.service.ts

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Post } from '../models/post.model'; #Cuidado aquí

@Injectable({ providedIn: 'root' })
export class PostsService {
  private readonly url = 'https://jsonplaceholder.typicode.com/posts';

  constructor(private http: HttpClient) {}

  getAll(): Observable<Post[]> {
    return this.http.get<Post[]>(this.url);
  }
}

5.1. Componente principal (consumo del GET)

Idea clave:

  • El componente inyecta el servicio

  • No hace la petición directamente

  • Guarda el resultado en un Observable

src/app/app.component.ts

import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { Observable } from 'rxjs';
import { AsyncPipe } from '@angular/common';

import { PostsService } from './services/posts.service'; #Cuidado aquí
import { Post } from './models/post.model'; #Cuidado aquí

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, AsyncPipe],
  templateUrl: './app.component.html',
})
export class AppComponent {
  posts$: Observable<Post[]>;

  constructor(private postsService: PostsService) {
    this.posts$ = this.postsService.getAll();
  }
}

5.2. Plantilla (mostrar los posts)

Aquí trabajamos:

  • @for → Angular 17+

  • async pipe → sin subscribe

  • Flujo reactivo limpio

src/app/app.component.html

<h1>Listado de Posts</h1>
<p>GET básico · Angular moderno</p>

<ul>
  @for (post of posts$ | async; track post.id) {
    <li>
      <h3>{{ post.title }}</h3>
      <p>{{ post.body }}</p>
    </li>
  } @empty {
    <li>No hay posts</li>
  }
</ul>

5.3. Estilos mínimos

src/app/app.component.css

ul {
  list-style: none;
  padding: 0;
}

li {
  border: 1px solid #ddd;
  border-radius: 10px;
  padding: 12px;
  margin-bottom: 10px;
  background: #fff;
}

6. GET por ID con routing

Crear componentes
ng g c pages/posts-list --standalone
ng g c pages/post-detail --standalone
Crear rutas
import { Routes } from '@angular/router';
import { PostsListComponent } from './pages/posts-list/posts-list.component';
import { PostDetailComponent } from './pages/post-detail/post-detail.component';

export const routes: Routes = [
  { path: '', redirectTo: 'posts', pathMatch: 'full' },
  { path: 'posts', component: PostsListComponent },
  { path: 'posts/:id', component: PostDetailComponent },
  { path: '**', redirectTo: 'posts' },
];

6.1. Configurar main.ts con Router + HttpClient

src/main.ts

import { bootstrapApplication } from '@angular/platform-browser';
import { provideHttpClient } from '@angular/common/http';
import { provideRouter } from '@angular/router';

import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(),
    provideRouter(routes),
  ],
}).catch(console.error);

6.2. AppComponent con RouterOutlet

src/app/app.ts

import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet],
  template: ,
})
export class AppComponent {}

6.3. Servicio: añadir getById

Añadimos en src/app/services/posts.ts

getById(id: number) {
  return this.http.get<Post>(`${this.url}/${id}`);
}

https://jsonplaceholder.typicode.com/posts/6

6.4. Página LISTADO (con enlaces a detalle)

src/app/pages/posts-list/posts-list.ts

import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Observable } from 'rxjs';
import { Post } from '../../models/post';
import { PostsService } from '../../services/posts';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-posts-list',
  imports: [RouterLink,AsyncPipe],
  templateUrl: './posts-list.html',
  styleUrl: './posts-list.css',
})
export class PostsListComponent {
  posts$!: Observable<Post[]>;

  constructor(private postsService: PostsService) {
    this.posts$ = this.postsService.getAll();
  }
}

posts-list.component.html

<h1>Posts</h1>

<ul>
  @for (p of posts$ | async; track p.id) {
    <li>
      <a [routerLink]="['/posts', p.id]">{{ p.title }}</a>
    </li>
  } @empty {
    <li>No hay posts</li>
  }
</ul>

6.5. Página DETALLE (ActivatedRoute + GET by id)

src/app/pages/post-detail/post-detail.ts

import { Component } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Observable, switchMap } from 'rxjs';
import { PostsService } from '../../services/posts';
import { Post } from '../../models/post';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-post-detail',
  imports: [RouterLink, AsyncPipe],
  templateUrl: './post-detail.html',
  styleUrl: './post-detail.css',
})
export class PostDetailComponent {
  // 1) Escucho cambios en la URL
  // 2) saco el id
  // 3) hago getById(id)
   post$!: Observable<Post>;

  constructor(
    private route: ActivatedRoute,
    private postsService: PostsService
  ) {
    this.post$ = this.route.paramMap.pipe(
      switchMap(params => {
        const id = Number(params.get('id'));
        return this.postsService.getById(id);
      })
    );
  }
}

post-detail.component.html

<a routerLink="/posts" class="link">← Volver</a>

@if (post$ | async; as p) {
  <section class="box">
    <h2>{{ p.title }}</h2>
    <p>{{ p.body }}</p>

    <p class="meta">
      <b>ID:</b> {{ p.id }} · <b>User:</b> {{ p.userId }}
    </p>
  </section>
} @else {
  <p>Cargando post...</p>
}

rc/app/pages/post-detail/post-detail.component.css

.link {
  text-decoration: none;
  border-bottom: 1px dotted #888;
}

.box {
  border: 1px solid #eee;
  border-radius: 12px;
  padding: 14px;
  background: #fff;
  margin-top: 12px;
}

.meta {
  opacity: 0.8;
  margin-top: 10px;
}

7. DELETE desde el detalle

Explicación

7.1. Servicio: delete(id)

src/app/services/posts.service.ts

delete(id: number) {
  return this.http.delete<void>(`${this.url}/${id}`);
}

7.2. Detalle (PostDetail)

src/app/pages/post-detail/post-detail.component.ts

import { AsyncPipe } from '@angular/common';
import { Component } from '@angular/core';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Observable, switchMap } from 'rxjs';
import { Post } from '../../models/post';
import { PostsService } from '../../services/posts';

@Component({
  selector: 'app-post-detail',
  imports: [RouterLink,AsyncPipe],
  templateUrl: './post-detail.html',
  styleUrl: './post-detail.css',
})
export class PostDetailComponent {
       post$!: Observable<Post>;

       constructor(
        private route: ActivatedRoute,
        private router: Router,
        private postService: PostsService
       ){
          this.post$ = this.route.paramMap.pipe(
            switchMap(params => {
              const id = Number(params.get('id'));
              return this.postService.getById(id);
            })
          )
       }
         deletePost(id: number) {
    const ok = confirm('¿Seguro que quieres borrar este post?');
    if (!ok) return;

    this.postService.delete(id).subscribe({
      next: () => this.router.navigate(['/posts']),
      error: (e) => console.error('Error borrando:', e),
    });
  }
}

7.3. post-detail.component.html

<a routerLink="/posts" class="link">← Volver</a>

@if (post$ | async; as p) {
  <section class="box">
    <h2>{{ p.title }}</h2>
    <p>{{ p.body }}</p>

    <p class="meta">
      <b>ID:</b> {{ p.id }} · <b>User:</b> {{ p.userId }}
    </p>

    <div class="actions">
      <button type="button" class="btn danger" (click)="deletePost(p.id)">
        Borrar
      </button>
    </div>
  </section>
} @else {
  <p>Cargando post...</p>
}

8. POST (crear)

Vamos a crear

  • Un componente nuevo: PostFormComponent

  • ReactiveFormsModule

  • Un método create() en el servicio

  • Navegación al detalle tras crear

8.1. Servicio: post

src/app/services/posts.service.ts

create(post: Post) {
  return this.http.post<Post>(this.url, post);
}

8.2. Componente de formulario

ng g c pages/post-form --standalone

Y lo añadimos a la ruta (posts/new debe ir antes que posts/:id)

src/app/app.routes.ts

import { PostFormComponent } from './pages/post-form/post-form.component';

export const routes: Routes = [
  { path: '', redirectTo: 'posts', pathMatch: 'full' },
  { path: 'posts', component: PostsListComponent },
  { path: 'posts/new', component: PostFormComponent },   // Aquí
  { path: 'posts/:id', component: PostDetailComponent },
  { path: '**', redirectTo: 'posts' },
];

8.3. Lógica de en TS

src/app/pages/post-form/post-form.component.ts

import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { Router, RouterLink } from '@angular/router';
import { PostsService } from '../../services/posts';
import { Post } from '../../models/post';

@Component({
  selector: 'app-post-form',
  imports: [ReactiveFormsModule,RouterLink],
  templateUrl: './post-form.html',
  styleUrl: './post-form.css',
})
export class PostFormComponent {
  form = new FormGroup({
    title: new FormControl('',{nonNullable: true}),
    body: new FormControl('', {nonNullable: true})
  })
constructor(
    private postsService: PostsService,
    private router: Router
  ) {}

  save() {
    const payload: Post = {
      userId: 1,
      title: this.form.controls.title.value,
      body: this.form.controls.body.value,
      id: 0, // no es necesario, pero no molesta
    };

    this.postsService.create(payload).subscribe({
      next: (created) => this.router.navigate(['/posts', created.id ?? 1]),
      error: (e) => console.error('Error creando:', e),
    });


}
}

8.4. Plantilla HTML

src/app/pages/post-form/post-form.component.html

<a routerLink="/posts" class="link">← Volver</a>

<h2>Nuevo post</h2>

<form class="form" [formGroup]="form" (ngSubmit)="save()">
  <label>
    Título
    <input type="text" formControlName="title" />
  </label>

  <label>
    Contenido
    <textarea rows="6" formControlName="body"></textarea>
  </label>

  <button class="btn" type="submit">Crear</button>
</form>

Metemos estilos:

.link { text-decoration: none; border-bottom: 1px dotted #888; }
.form { display: grid; gap: 10px; max-width: 520px; margin-top: 12px; }
input, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 10px; }
.btn { padding: 10px 12px; border: 1px solid #ddd; border-radius: 10px; background: #fff; cursor: pointer; }

8.5. Añadir enlace en el listado

posts-list.component.html arriba

<a routerLink="/posts/new">+ Nuevo post</a>

9. PUT (editar)

Un solo PostFormComponent sirve para:

  • Crear: /posts/new

  • Editar: /posts/:id/edit

9.1. Servicio: update(id, post)

src/app/services/posts.service.ts

update(id: number, post: Post) {
  return this.http.put<Post>(`${this.url}/${id}`, post);
}

9.2. Ruta nueva: /posts/:id/edit

Después de posts/new y antes de posts/:id:

{ path: 'posts/:id/edit', component: PostFormComponent },
{ path: 'posts', component: PostsListComponent },
{ path: 'posts/new', component: PostFormComponent },
{ path: 'posts/:id/edit', component: PostFormComponent },
{ path: 'posts/:id', component: PostDetailComponent },

9.3. PostFormComponent: crear vs editar

Vamos a reutilizar el componente:

  • Si hay :ideditar

  • Si no hay :idcrear

import { Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { PostsService } from '../../services/posts';
import { Post } from '../../models/post';
import { of, switchMap } from 'rxjs';

@Component({
  selector: 'app-post-form',
  imports: [ReactiveFormsModule,RouterLink],
  templateUrl: './post-form.html',
  styleUrl: './post-form.css',
})
export class PostFormComponent {
  postId?: number;

  form = new FormGroup({
    title: new FormControl('',{nonNullable: true}),
    body: new FormControl('', {nonNullable: true})
  })
constructor(
    private route: ActivatedRoute,
    private postsService: PostsService,
    private router: Router
  ) {
// Si hay :id => modo editar => cargamos el post y rellenamos el form
    this.route.paramMap.pipe(
      switchMap(params => {
        const id = params.get('id');
        if (!id) return of(null);
        this.postId = Number(id);
        return this.postsService.getById(this.postId);
      })
    ).subscribe(post => {
      if (post) {
        this.form.patchValue({
          title: post.title,
          body: post.body,
        });
      }
    });

  }

  save() {
    const payload: Post = {
      userId: 1,
      title: this.form.controls.title.value,
      body: this.form.controls.body.value,
      id: this.postId ?? 0, // no es necesario, pero no molesta
    };

    // si hay postId => PUT, si no => POST
    const request$ = this.postId
      ? this.postsService.update(this.postId, payload)
      : this.postsService.create(payload);

    request$.subscribe({
      next: (res) => this.router.navigate(['/posts', res.id ?? this.postId ?? 1]),
      error: (e) => console.error('Error guardando:', e),
    });


}
}

9.4. HTML: cambia el título y botón según modo

post-form.component.html

<a routerLink="/posts" class="link">← Volver</a>

<h2>{{ postId ? 'Editar post' : 'Nuevo post' }}</h2>

<form class="form" [formGroup]="form" (ngSubmit)="save()">
  <label>
    Título
    <input type="text" formControlName="title" />
  </label>

  <label>
    Contenido
    <textarea rows="6" formControlName="body"></textarea>
  </label>

  <button class="btn" type="submit">
    {{ postId ? 'Guardar cambios' : 'Crear' }}
  </button>
</form>

Añadir link “Editar” en el detalle, junto al botón borrar: (PostDetail)

<div class="actions">
  <a class="btn" [routerLink]="['/posts', p.id, 'edit']">Editar</a>

  <button type="button" class="btn danger" (click)="deletePost(p.id)">
    Borrar
  </button>
</div>

10. Búsqueda + filtrado

Trabajamos

  • Reactive Forms

  • RxJS básico (combineLatest + map)

  • async pipe

  • Mantenemos el listado reactivo y limpio

Lo que vamos a ver:

  • Filtrar datos sin tocar la API

  • Formularios modernos: FormControl

  • Flujo reactivo:

    • valueChanges

    • combineLatest

    • map

    • async pipe

10.1. Lógica del post-list

import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { Post } from '../../models/post';
import { PostsService } from '../../services/posts';
import { AsyncPipe } from '@angular/common';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { Observable, combineLatest, map, startWith } from 'rxjs';

@Component({
  selector: 'app-post-list',
  imports: [AsyncPipe,RouterLink,ReactiveFormsModule],
  templateUrl: './post-list.html',
  styleUrl: './post-list.css',
})
export class PostListComponent {
  posts$!: Observable<Post[]>;

  searchControl = new FormControl('', { nonNullable: true });
  userIdControl = new FormControl<number | null>(null);
  sortControl = new FormControl<'id-asc' | 'id-desc' | 'title-asc' | 'title-desc'>(
    'id-asc',
    { nonNullable: true }
  );

  filteredPosts$!: Observable<Post[]>;

 constructor(private postsService: PostsService) {
    //  Inicialización segura
    this.posts$ = this.postsService.getAll();

    this.filteredPosts$ = combineLatest([
      this.posts$,
      this.searchControl.valueChanges.pipe(startWith(this.searchControl.value)),
      this.userIdControl.valueChanges.pipe(startWith(this.userIdControl.value)),
      this.sortControl.valueChanges.pipe(startWith(this.sortControl.value)),
    ]).pipe(
      map(([posts, term, userId, sort]) => {
        const q = term.trim().toLowerCase();

        const filtered = posts.filter(p => {
          const matchesText =
            !q || (p.title + ' ' + p.body).toLowerCase().includes(q);

          const matchesUser =
            userId === null || p.userId === userId;

          return matchesText && matchesUser;
        });

        const sorted = [...filtered];
        sorted.sort((a, b) => {
          switch (sort) {
            case 'title-asc':
              return a.title.localeCompare(b.title);
            case 'title-desc':
              return b.title.localeCompare(a.title);
            case 'id-desc':
              return b.id - a.id;
            default:
              return a.id - b.id;
          }
        });

        return sorted;
      })
    );
}
  clear() {
    this.searchControl.setValue('');
    this.userIdControl.setValue(null);
    this.sortControl.setValue('id-asc');
  }
}

10.2. HTML

Añadimos buscador y trabajamos con filteredPosts$

<header class="top">
  <h1>Posts</h1>
  <a class="btn" routerLink="/posts/new">+ Nuevo post</a>
</header>

<div class="search">
  <input
    type="text"
    [formControl]="searchControl"
    placeholder="Buscar por título o contenido…"
  />
</div>

<ul class="list">
  @for (p of filteredPosts$ | async; track p.id) {
    <li class="item">
      <a [routerLink]="['/posts', p.id]">{{ p.title }}</a>
      <p class="body">{{ p.body }}</p>
    </li>
  } @empty {
    <li>No hay posts</li>
  }
</ul>

CSS bonito

.top { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.btn { padding:8px 12px; border:1px solid #ddd; border-radius:10px; text-decoration:none; background:#fff; }

.search { margin: 12px 0; }
.search input { width: 100%; max-width: 520px; padding: 10px; border: 1px solid #ddd; border-radius: 10px; }

.list { list-style: none; padding: 0; margin: 0; }
.item { padding: 12px; border: 1px solid #eee; border-radius: 12px; margin-bottom: 10px; background: #fff; }
.body { opacity: 0.8; margin: 6px 0 0; }
a { text-decoration: none; }