Introducción - ¿Qué es un servicio?

Una pieza fundamental en la mayoría de webs y aplicaciones es la de la capa de los datos. El acceso a los datos lo puedes hacer perfectamente desde los propios componentes, pero, lo recomendable es que esta capa de acceso a los datos esté separada.

Este acceso a los datos puede ser de cualquier tipo: datos estáticos, datos de una API o backend, datos de websockets… etc. Si un día decides cambiar de lugar de los datos, por ejemplo, quitar una API y poner websockets vas a agradecer mucho que tengas una estructura con el acceso a los datos separados.

Lo que se hace en Angular es crear un fichero separado para esto llamado servicio. Los servicios son clases que se encargan de acceder a los datos para entregarlos a los componentes. Lo bueno de esto es que puedes reaprovechar servicios para distintos componentes.

Cómo se crean los servicios de Angular

Para crear un servicio, podemos crear manualmente un archivo llamado nombre-componente.service.ts o podemos dejar que angular cli lo cree por nosotros:

ng generate service nombre-componente --module=app

El atributo –module=app indica que el servicio que se va a crear se va a importar directamente en el app.module.ts. Si lo creas de forma manual o no pones este parámetro tienes que importar tu servicio en el app.module.ts:

En ele ejemplo de abajo he creado un componente llamado Messages dentro de una carpeta con el mismo nombre y dentro he creado el servico también con el nombre messages. Los servicios se importan en la sección providers del app.module.ts

import { BrowserModule } from "@angular/platform-browser";
import { NgModule } from "@angular/core";
import { AppRoutingModule } from "./app-routing.module";
import { AppComponent } from "./app.component";
import { MessagesService } from "../messages/messages.service";
import { MessagesComponent } from "../messages/messages.component";
@NgModule({
  declarations: [AppComponent, MessagesComponent],
  imports: [BrowserModule, AppRoutingModule],
  providers: [MessagesService],
  bootstrap: [AppComponent]
})
export class AppModule {}

Un servicio tiene la siguiente estructura:

// messages/messages.service.ts

import { Injectable } from "@angular/core";

@Injectable({
  providedIn: "root"
})
export class MessageService {
  messages: string[] = [];

  add(message: string) {
    this.messages.push(message);
  }

  clear() {
    this.messages = [];
  }
}

Lo primero es importar Injectable para poder usar esa etiqueta de Angular y así poder inyectar el servicio. En ese caso, dentro del inyectable he puesto providedIn: 'root' para que el servicio se autoimporte y no tengas que importarlo en el app.module.ts.

Debajo se pone el export con el nombre del servicio, normalmente es NombreComponentService. Bien, tienes que pensar que se va a usar esta clase para acceder a los datos. Para este ejemplo he creado un array de mensajes inicializado a array vacío, justo debajo del export. Ese array serán nuestros datos.

Por último se sitúan los métodos de añadir mensajes y borrar todos los mensajes (aquí tienes que poner los que necesites para acceder o modificar tus datos).

Cómo usar los servicios dentro de los componentes

La anotación @Injectable indica que el servicio puede ser inyectado mediante inyección de dependencias tal y como vimos en el artículo anterior, es decir, primero tenemos que importar el servicio en el componente:

import { ServiceName } from "../serviceName.service";

A continuación lo inyectamos en el constructor del componente:

constructor(private service: ServiceName) { }

Ya podemos usar los métodos del servicio en el componente service.nombreFuncion().

Por ejemplo en el ejemplo del servicio de los mensajes, dentro del componente que use ese servicio:

// messages/messages.component.ts

import { Component, OnInit } from "@angular/core";
import { MessageService } from "../message.service";

@Component({
  selector: "app-messages",
  templateUrl: "./messages.component.html",
  styleUrls: ["./messages.component.css"]
})
export class MessagesComponent implements OnInit {
  constructor(public messageService: MessageService) {}

  ngOnInit() {
    this.messageService.add("prueba");
  }
}

Llamadas HTTP a una API REST. Cómo usar HTTPClient

Hasta ahora con lo que sabemos podemos usar los servicios para leer y escribir datos, pero simulados o guardados en la memoria del navegador, para leer o escribir de una API REST tenemos que hacer llamadas HTTP.

Por cierto, una API es un conjunto de endpoints (o rutas) que provee un servidor (backend) que permiten acceder a datos o realizar operaciones. Normalmente lo que se suele hacer es primero generar una API en un servidor usando un lenguaje como Java o Python (ojo porque no puedes generar una API con Angular, tiene que estar creada en un servidor).

Si no tienes una API y no sabes cómo crear una, te dejo esta API de ejemplo que te puede servir para aprender y que además vamos a usar en este artículo:

API Fake de ejemplo

Antes de empezar con las llamadas hay que conocer que Angular tiene un modulo que facilita esta tarea, el modulo es HttpClient. Con este modulo no necesitas usar fetch ni ajax ni nada.

Para usar HttpClient de Angular en cualquier parte, tenemos que importar el módulo HttpClientModule, en la sección imports de el app.module.ts:

import { HttpClientModule } from "@angular/common/http";

HttpClient usa Observables de RxJS. Los observables son una colección de futuros eventos que llegan de forma asíncrona. Si quieres aprender más de RxJS puedes visitar su web oficial: http://reactivex.io/rxjs/.

Aunque lo hayamos importado en la página de forma global, también tenemos que importarlo y inyectarlo en los constructores de los servicios desde los que vayamos a realizar llamadas HTTP. Por ejemplo para este nuevo servicio que he creado para listar los usuarios de que vienen de la API:

// users/users.service.ts

import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient) {}
}

Con eso listo ya puedes realizar llamadas HTTP. Por si no conocías las llamadas HTTP te las presento:

  • GET: Simplemente devuelven información.
  • POST: A estos endpoints se envía información normalmente para crear o ejecutar acciones sobre recursos en bases de datos.
  • PUT: Se envía información al endpoint y se modifica en base de datos un recurso.
  • DELETE: Para borrar recursos del servidor.

Ahora para realizar llamadas http podemos invocar cualquiera de los siguiente métodos definidos en el HttpClient:

  • get()
  • post()
  • put()
  • delete()

Por ejemplo:

// users/users.service.ts

constructor(private http: HttpClient){
  this.http.get('https://reqres.in/api/users?page=2');
}

LLamadas GET

Son las llamadas más básicas y como hemos dicho sirven para devolver información desde el servidor. Esta información puede ser un dato simple, un objeto o una lista de objetos todo ello en formato JSON normalmente.

Por ejemplo para hacer una llamada get a la API de ejemplo que hemos comentado antes:

getUsers(){
  this.http.get('https://reqres.in/api/users?page=2').subscribe(data => {
    console.log(data);
  });
  console.log("Esto se ejecutará antes que el console log de arriba");
}

Fíjate que ahora después de la llamada al método get() usamos subscribe. Esto funciona como las promesas de Javascript, con ese método te esperas a que la petición termine. Dentro de ese método se ejecuta una función (usando arrow functions) en la que se devuelve el objeto data que contiene la respuesta a la petición de la API.

El console.log de abajo se ejecuta antes incluso de que termine la petición. Esto pasa porque el código es asíncrono y por tanto lo que pongas debajo no va a esperar a que la petición termine. Dentro del subscribe si que tienes la certeza de que la petición ha terminado y por tanto tienes la respuesta.

El servicio completo quedaría así:

// users/users.service.ts

import { HttpClient } from "@angular/common/http";

@Injectable({
  providedIn: "root"
})
export class UsersService {
  constructor(private http: HttpClient) {}

  getUsers() {
    this.http.get("https://reqres.in/api/users?page=2").subscribe(data => {
      console.log(data);
    });
  }
}

Ahora podemos inyectar este servicio como ya hemos hecho, en este caso en el componente de lista de usuarios:

// users/users.component.ts

import { Component, OnInit } from "@angular/core";
import { UsersService } from "./users.service";

@Component({
  selector: "app-users",
  templateUrl: "./users.component.html",
  styleUrls: ["./users.component.css"]
})
export class UsersComponent implements OnInit {
  constructor(public usersService: UsersService) {}

  ngOnInit() {
    this.usersService.getUsers();
  }
}

La consola del navegador muestra una lista de objetos que vienen desde la API

Con esto podemos realizar llamadas desde el servicio pero los datos que se devuelven todavía no los tenemos en el componente para poder mostrarlos.

Para hacer poder mostrar los datos en el HTML del componente, en lugar de hacer el subscribe en el servicio, tenemos que devolver un Observable de la llamada http, es decir:

// users/users.service.ts

getUsers(): Observable<any>{
  return this.http.get('https://reqres.in/api/users?page=2');
}

No sin antes importar los observables en el servicio:

// users/users.service.ts

import { Observable } from "rxjs/Observable";

Para este ejemplo he puesto que el Observable sea de tipo Any (cualquier tipo de objeto) pero lo suyo, en un futuro, sería usar una interfaz para poder tener un modelo para los datos.

Ahora, en el componente, cuando queramos llamar al servicio (siempre y cuando lo hayamos inyectado en el controlador), tenemos que subscribirnos para recibir la información, es decir:

// users/users.component.ts

import { Component, OnInit } from "@angular/core";
import { UsersService } from "../users.service";

@Component({
  selector: "app-users",
  templateUrl: "./users.component.html",
  styleUrls: ["./users.component.css"]
})
export class UsersComponent implements OnInit {
  users: any;

  constructor(public usersService: UsersService) {}

  ngOnInit() {
    this.usersService.getUsers().subscribe(data => {
      this.users = data;
    });
  }
}

IMPORTANTE Cuando carga un componente, si mostramos una variable que viene de una petición HTTP, no se cargará y tirará error porque en el instante en el que se abre la página, la petición aún no se ha realizado. Para arreglar esto tenemos que poner un ngIf a la variable que viene desde la petición antes de mostrarla en la vista:

<!-- users/users.component.html -->

<div *ngIf="users">
  {{ users }}
</div>

LLamadas POST y PUT

Las llamadas POST y PUT sirven para enviar información al servidor y que éste nos responda. POST se usa para crear recursos, por ejemplo, crear usuarios, crear artículos o lo que sea. Normalmente se pasa un objeto o conjunto de objetos a crear. Las peticiones PUT sirven para actualizar un objeto ya creado. Se pasa lo mismo que en el post, un objeto o conjunto de ellos para editar por los que haya se hayan creado.

Para conseguir esto simplemente al realizar la llamada que corresponda, desde el servicio pasamos el objeto correspondiente, por ejemplo:

// users/users.service.ts

createUser(user: Any): Observable<any>{
  return this.http.post('https://reqres.in/api/users', user);
}
editUser(user: Any): Observable<any>{
  return this.http.post('https://reqres.in/api/users/2', user);
}

El segundo parámetro de la función del get y del post es el objeto o objetos que quieres enviar al servidor.

Con esto ya podemos usarlo dentro del componente de users:

// users/users.component.ts

import { Component, OnInit } from "@angular/core";
import { UsersService } from "../users.service";

@Component({
  selector: "app-users",
  templateUrl: "./users.component.html",
  styleUrls: ["./users.component.css"]
})
export class UsersComponent implements OnInit {
  users: any;

  constructor(public usersService: UsersService) {}

  ngOnInit() {
    this.usersService.createUser({
      name: "morpheus",
      job: "leader"
    });
    this.usersService.editUser({
      name: "morpheus",
      job: "zion resident"
    });
  }
}

Por cierto, en los post y los put también puedes hacer un subscribe para esperar a que termine la petición por si quieres ejecutar algo después o recibir la información del servidor, la sintaxis es la misma que con el GET.

LLamadas DELETE

La llamada DELETE se usa para borrar recursos del servidor y si suntaxis es exactamente igual que las que hemos visto anteriormente:

// users/users.component.ts

deleteUser(): Observable<any>{
  return this.http.delete('https://reqres.in/api/users/2');
}

Enviar headers de autorización en las peticiones

Vale, con lo que hemos visto ahora te sirve para usar muchas APIS, pero puede darse el caso en el que tu API esté protegida con un sistema de Basic auth. En ese caso, para no tener que pasar esa información en cada petición lo que puedes hacer es extender HttpClient para pasar estos headers en todas las peticiones sin que te tengas que acordar de hacerlo cada vez.

Yo lo que hice en su día (puede haber más soluciones y mejores) fué crear un archivo .ts para pasar en todas las llamadas los headers:

import { Injectable, OnInit } from "@angular/core";
import { Http, Headers, RequestOptions } from "@angular/http";

@Injectable()
export class HttpWithHeaders {
  public sessionData: SessionData;

  constructor(private http: Http) {
    this.headers = "Basic " + btoa("username" + ":" + "password");
  }
  get(url) {
    return this.http.get(url, {
      headers: this.generateHeaders()
    });
  }
  post(url, data) {
    return this.http.post(url, data, {
      headers: this.generateHeaders()
    });
  }
  put(url, data) {
    return this.http.put(url, data, {
      headers: this.generateHeaders()
    });
  }
  delete(url) {
    return this.http.delete(url, {
      headers: this.generateHeaders()
    });
  }
}

Para usar esta clase que acabamos se crear, simplemente tenemos que cambiar en los servicios el httpClient por nuestro HttpWithHeaders.

Agradecimientos a @ExtremoBlando por enseñarme ésta técnica.

Lo único que faltaría es cambiar lo de “username” y “password” para que use el usuario y la contraseña del usuario que está logueado.

Tratamiento de errores en las llamadas

Al hacer subscribe, ya sea en el servicio o en el controlador, podemos poner un parámetro para saber si se ha producido un error:

data => {
  this.data = data;
  console.log(data);
},
err => {
  console.log("Error.");
};

Si necesitas más información específica, puedes declarar el parámtro de eror de tipo HttpErrorResponse (lo tienes que importar desde @angular/common/http), por ejemplo:

this.http
  .get<UserResponse>("https://api.github.com/users/seeschweiler")
  .subscribe(
    data => {
      console.log("User Login: " + data.login);
      console.log("Bio: " + data.bio);
      console.log("Company: " + data.company);
    },
    (err: HttpErrorResponse) => {
      if (err.error instanceof Error) {
        console.log("Client-side error");
      } else {
        console.log("Server-side error");
      }
    }
  );

Aquí he usado la API de Github. UserResponse es una interfaz que tiene los campos que devuelve esta petición.

Conclusiones

Con lo que hemos ido viendo en los capítulos anteriores ya conoces lo básico de Angular. Te recomiendo que practiques mucho creando webs aunque sean de ejemplo o para pasar el rato ya que así afianzas mucho los conocimientos.

En próximos capítulos veremos un ejemplo práctico para hacer un sistema de login de usuarios y registro de los mismos.