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 -o3. 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/66.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 :id → editar
-
Si no hay :id → crear
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; }