Comunicación entre Angular y un Servidor utilizando una API REST y httpclient

En este proyecto vamos a realizar una serie de ejemplo de cómo trabajar con una API REST en Angular. Ya anteriormente hemos visto realizar este tipo de comunicaciones con la librería XMLHttpRequest pero realmente con este tutorial queremos evolucionar y mejorar nuestras aplicaciones utilizando HttpClient de Angular.

Con esta mejora lo que pretendemos es que la captación de eventos y las peticiones a la API sea mucho más rápidas y eficientes haciendo comunicaciones asíncronas y captando los errores producidos. La principal consecuencia de esta evolución la puedes percibir en la experiencia de usuario. La aplicación continue en funcionamiento mientras espera la respuesta de la petición realizada al servidor, es decir, tenemos peticiones en segundo plano.

El código utilizado en este artículo lo puedes obtener del siguiente repositorio https://github.com/al118345/TestComunicacionApiRest_Angular. El código utilizado en la API REST lo puedes obtener del siguiente repositorio: https://github.com/al118345/envio_email_api_python

Ejemplo petición GET

Antes de empezar, si quieres descargar el ejemplo explicado en este punto visita el siguiente enlace: https://github.com/al118345/TestComunicacionApiRest_Angular/tree/master/src/app/components/saludar-servidor

Esta petición es la más básica que podemos realizar. Por ejemplo podemos hacer una petición GET a nuestro servidor para que nos devuelva un hola, bienvenido.

La estructura la petición podría ser la siguiente:

  constructor(private http: HttpClient) { }

   /*
  Cómunicación con una API con el objetivo de obtener un saludo de bienvenida.
  return: Observable<any>
   */
  saludo(): Observable<any>  {
      return this.http.get<any>( url_api_rest/saludo/inicial)
        .pipe(
          catchError((err) => {
            console.log('error caught in service')
            console.error(err);
            return throwError(err);
          })
        );
  }

Yo normalmente creo un servicio que posteriormente es utilizado desde el componente.ts, por ejemplo, en este caso he creado ConsultarapiService que lo he utilizado de la siguiente forma:

  cargando = false;
  resultado = '';
  printado = false;
  object_json: JsonObject = {};

  constructor(private api: ConsultarapiService) { }

  get_saludo() {
    const auxiliar = this.api.saludo();
    this.resultado = '';
    this.printado = false;
    this.cargando  = true;
    this.object_json = {};
    auxiliar.subscribe({
      complete: () => {
        this.cargando = false ;
      },
      error: () => {
        this.printado = true;
        this.cargando = false ;
        this.resultado = 'Error en la comunicación con el servidor';
      },
      next: (resultado) => {
        const aux = resultado as JsonObject;
        this.object_json = aux;
        this.resultado = 'Respuesta recibida correctamente. Esta es la respuesta.'
        this.printado = true;
      }
    });
  }

Por último, los eventos en la vista los podemos gestionar visualmente de la siguiente forma:

<button type="submit" [disabled]="cargando" class="btn btn-primary"
          (click)="get_saludo()">Obtener Saludo</button>
<ng-container *ngIf="printado">
  <div class="alert-info">
    <p>{{this.resultado}}</p>
    <pre>{{this.object_json | json}}</pre>
  </div>
</ng-container>
<ng-container *ngIf="cargando">
  <mat-spinner  mode="indeterminate" ></mat-spinner>
</ng-container>

Prueba este ejemplo con la ejecución de este botón:

Fíjate que existe un tiempo de espera en que la aplicación no está parada y aparece un cargando. Esto se debe a que httpclient se encarga de realizar la llama asíncrona y, además, de gestionar todos los errores que puedan ocurrir

Ejemplo petición POST - Login

Antes de empezar, si quieres descargar el ejemplo explicado en este punto visita el siguiente enlace: https://github.com/al118345/TestComunicacionApiRest_Angular/tree/master/src/app/components/login

Otro ejemplo de petición POST es la de enviar un formulario con usuario y contraseña para realizar un login en nuestro servidor. Por ejemplo queremos realizar una autenticación OAuth2.0 (más información en Wikipedia https://es.wikipedia.org/wiki/OAuth) en nuestro servidor y, por lo tanto, enviamos el usuario y la contraseña con el siguiente comando:


/*
Cómunicación con una API de ejemplo para obtener un token de autenticación.
return : Observable<User>
*/
login(username: string, password: string): Observable<User>  {
      const data = {
        'user': username,
        'password': password
      };
      const httpOptions = {
        headers: new HttpHeaders({
          'Content-Type':  'application/json',
        })
      };
      return this.http.post<User>( url_api_rest/login
        , data, httpOptions)
        .pipe(
          catchError((err) => {
            console.log('error caught in service')
            console.error(err);
            return throwError(err);
          })
        );
  }

Si se observa el código, lo que hacemos es enviar una petición POST con la información del usuario y contraseña para autentificarnos en el sistema.

El resultado obtenido es un JSON con un token de autenticación. Hemos definido un modelo usuario en el sistema con la siguiente configuración:

export class User {
  constructor(
    public user: string,
    public passwd: string,
    public token: string
  ) {
  }
}

La respuesta del servidor es mapeada en el modelo usuario. En ese momento tenemos acceso al token que nos permite realizar las peticiones que queramos al servidor.


public  token = '';
loading = false;
error = false;
username = '';
password = '';
show = false;


constructor(public api: ConsultarapiService) { }
ngOnInit(): void {
      this.token = '';
}

login() {
    const auxiliar = this.api.login(this.username, this.password)
    this.show = false;
    this.loading = true;
    this.error = false;
    auxiliar.subscribe({
      complete: () => {
        this.loading = false;
      },
      error: () => {
        this.error = true;
        console.log('error en el servicio, dejamos entrar')
      },
      next: (resultado) => {
        this.token = resultado.token;
        if (this.token  !== null ) {
          this.show = true;
        }
        else{
          this.error = true;
        }
      }
    });
  }

Ejemplo petición POST con filtrado de la respuesta recibida utilizando map.

Antes de empezar, si quieres descargar el ejemplo explicado en este punto visita el siguiente enlace: https://github.com/al118345/TestComunicacionApiRest_Angular/tree/master/src/app/components/obtener-json

El título de esta sección es extraño de explicar pero con un ejemplo se entenderá perfectamente. Realizamos una petición POST a la API y el servidor nos puede devolver una estructura de respuesta cómo la siguiente:

{
	"links": {
		"self": "http://example.com/articles",
		"next": "http://example.com/articles?page[offset]=2",
		"last": "http://example.com/articles?page[offset]=10"
	},
	"datos": {
		"type": "articles",
		"id": "1",
		"attributes": {
			"title": "JSON:API paints my bikeshed!"
		},
		"relationships": {
			"author": {
				"links": {
					"self": "http://example.com/articles/1/relationships/author",
					"related": "http://example.com/articles/1/author"
				},
				"data": {
					"type": "people",
					"id": "9"
				}
			},
			"comments": {
				"links": {
					"self": "http://example.com/articles/1/relationships/comments",
					"related": "http://example.com/articles/1/comments"
				},
				"data": [{
						"type": "comments",
						"id": "5"
					},
					{
						"type": "comments",
						"id": "12"
					}
				]
			}
		}
	}
}
    

De la respuesta obtenida únicamente queremos obtener la información almacenada en la key datos. Para ello utilizamos el siguiente código.

ejemploPeticionPost(): Observable<any> {

    const httpOptions = {
      headers: new HttpHeaders({
        'Content-Type':  'application/json',
      })
    };
    return this.http
      .post<any>(api_url, httpOptions)
      .pipe(
        map((response) => response.datos),
        catchError((err) => {
          console.log('error caught in service')
          console.error(err);
          return throwError(err);
        })
      );
  }

Observa que la propia respuesta de la API tiene una key llamada datos. Esta key contiene la información que nos interesa y, por lo tanto, mapeamos la respuesta para que el método devuelva no el objeto completo sino la parte del Json que nos interesa.

A continuación muestro la vista correctamente configurada:

 <div class="row">
  <div class="col-md-10">
    Ejemplo de obtención de un documento JSON desde una API REST
  </div>

</div>
<p></p>
<div class="row">
  <div class="col-md-5">
    <button type="submit" [disabled]="cargando" class="btn btn-primary"
            (click)="obtener_json()">Obtener JSON</button>
    <ng-container *ngIf="printado">
      <div class="alert-info">
        <p>{{this.resultado}}</p>
        <pre>{{this.object_json | json}}</pre>
      </div>
    </ng-container>
    <ng-container *ngIf="cargando">
      <mat-spinner  mode="indeterminate" ></mat-spinner>
    </ng-container>
  </div>
</div>
 

Y el componente.ts


  cargando = false;
  resultado = '';
  printado = false;
  object_json: JsonObject = {};
  constructor(private api: ConsultarapiService) { }

  obtener_json() {
    const auxiliar = this.api.ejemploPeticionPost();
    this.resultado = '';
    this.printado = false;
    this.cargando  = true;
    this.object_json = {};
    auxiliar.subscribe({
      complete: () => {
        this.cargando = false ;
      },
      error: () => {
        this.printado = true;
        this.cargando = false ;
        this.resultado = 'Error en la comunicación con el servidor';
      },
      next: (resultado) => {
        const aux = resultado as JsonObject;
        this.object_json = aux;
        this.resultado = 'Respuesta recibida correctamente. Esta es la respuesta.'
        this.printado = true;
      }
    });
  }

  ngOnInit(): void {
  } 

Envío de ficheros a una API con un formulario angular y recepción del mismo.

Antes de empezar, si quieres descargar el ejemplo explicado en este punto visita el siguiente enlace: https://github.com/al118345/TestComunicacionApiRest_Angular/tree/master/src/app/components/cargarfichero

En esta circunstancia, entendemos que tenemos un formulario dónde obtenemos un fichero que debemos enviar a través de una api a nuestro servidor. Por ejemplo, podemos crear el siguiente formulario:
<h5><u><i>EJemplo cargar fichero</i></u></h5>
<p></p>
<p></p>
<div class="row">

  <form [formGroup]="myForm" (ngSubmit)="enviar_fichero()">
    <p></p>
    <div class="form-group">
      <label for="file">Fichero a enviar</label>
      <p></p>
      <p></p>
      <input formControlName="file" id="file" type="file" class="form-control" (change)="onFileChange($event)">
      <p></p>
      <p></p>
      <div *ngIf="f.file.touched && f.file.invalid" class="alert alert-danger">
        <div *ngIf="f.file.errors?.required">Es necesario un fichero.</div>
      </div>
      <p></p>
    </div>
    <p></p>

    <div *ngIf="error" class="form-group has-error">
      <p>{{this.texto_errocr}}</p>
    </div>

    <button class="btn btn-primary"  [disabled]="desactivar ||  robot " type="submit">Enviar Documento. </button>
    <div *ngIf="this.correcto then   mostrarinfo"></div>
    <ng-template #mostrarinfo>
      <div class="alert alert-success">
        <p>{{this.texto_correcto}}</p>
      </div>
    </ng-template >
    </form>

</div>

Posteriormente, el component.ts tiene que gestionar este fichero y almacenarlo en una variable Formgroup:

 onFileChange(event: any) {
    if (event.target.files.length > 0) {
      const file = event.target.files[0];
      this.myForm.patchValue({
        fileSource: file
      });
    }
  }

Una vez almacenado el fichero, podemos enviarlo a nuestro servidor a través de una petición a nuestra API cómo la siguiente implementación:


  /*
  Función para enviar un fichero a la api
  params: formData:  el formulario con el fichero
  return: Observable<any>
   */
  enviar_documento(formData:any): Observable<any> {
    const headers = new HttpHeaders().set('Accept', 'application/pdf');
    return this.http.post<any>(`http://0.0.0.0:5000/api/v1/ejemplo_envio_fichero/`, formData, {
      headers,
      responseType: 'blob' as 'json'
    }).pipe(
      catchError((err) => {

        console.log('error caught in service')
        console.error(err);
        return throwError(err);
      })
    );
  }

En este caso es un poco especial, porque envío un fichero y espero cómo respuesta espero otro modificado. Esta respuesta la quiero devolver directamente al usuario, por lo que la respuesta debe ser un observable. Además en la petición he añadido que responseType: 'blob' as 'json', es decir, devolveré un fichero en formato Json.

Esta respuesta la puedo devolver al usuario con el siguiente código:
myForm = new FormGroup({
    file: new FormControl('', [Validators.required]),
    fileSource: new FormControl('', [Validators.required])

  });
  get f() {
    return this.myForm.controls;
  }

  desactivar = false;
  error = false;
  texto_errocr = '';
  robot = false;
  correcto = false;
  texto_correcto = '';

  constructor(public api: ConsultarapiService) { }

  ngOnInit(): void {
    this.desactivar = false;
    this.error = false;
    this.texto_errocr = '';
    this.robot = false;
    this.correcto = false;
    this.texto_correcto = '';
   }


  enviar_fichero (){
    const formData = new FormData();
    formData.append('file', this.myForm.get('fileSource')?.value);


    const auxiliar = this.api.enviar_documento(formData)
    auxiliar.subscribe({
      complete: () => {
      },
      error: () => {
        this.error = true;
        this.texto_errocr = 'Error en el servicio, no se ha podido enviar el fichero';
        console.log('error en el servicio')
      },
      next: (response) => {
        this.correcto = true;
        this.texto_correcto = 'Fichero enviado correctamente';
        const dataType = response.type;
        const binaryData = [];
        binaryData.push(response);
        const downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, {type: dataType}));
        downloadLink.setAttribute('download', 'resultado_comprimido.pdf');
        document.body.appendChild(downloadLink);
        downloadLink.click();
      }
    });
  }

    

Recepción de ficheros desde una API-REST en Angular.

Antes de empezar, si quieres descargar el ejemplo explicado en este punto visita el siguiente enlace: https://github.com/al118345/TestComunicacionApiRest_Angular/tree/master/src/app/components/descargar-pdf

Imaginad que tenemos programado una API REST en nuestro servidor que permita descargar un fichero pdf. Por ejemplo, tenemos el siguiente código python en la api:
'''
Función para obtener un fichero de una API REST y devolverlo
'''
class DescargarFichero(Resource):
    def get(self):
        try:
            path = os.path.abspath(os.getcwd())+ '/ejemplo.pdf'
            return send_file(path)
        except Exception as error:
            response = jsonify({'status': 'error'})
            response.status_code = 422
            return response
api.add_resource(DescargarFichero, '/descargar_fichero')
La forma correcta de obtener el fichero de este método seria con un servicio.ts cómo el siguiente:

/*
Cómunicación con una API de ejemplo para obtener un pdf.
return : Observable<Blob>
*/
descargar_fichero () {
    const  httpOptions = new HttpHeaders({
        'Content-Type': 'application/pdf',
        Accept : 'application/pdf'});
    return this.http.get<Blob>( api_url,
      {headers: httpOptions, responseType: 'blob' as 'json' }   )
      ;
  }

A continuación os proporciono un botón de ejemplo para descargar un fichero pdf.

Qué tiene la siguiente configuración en la vista:


<section class="bg-light pt-5 text-secondary">
  <div class="container">
    <div class="align-items-center row">
      <div class="col-lg-6 mb-5">
        <p class="fw-light lead mb-4">Ejemplo descarga pdf</p><button [disabled]="cargando" (click)="descarga_fichero()" class="btn btn-primary">Descarga</button>
      </div>
      <div class="col-lg-6">
      </div>
    </div>
  </div>
  <ng-container *ngIf="printado">
    <div class="alert-info">
      <p>{{this.resultado}}</p>
    </div>
  </ng-container>
  <ng-container *ngIf="cargando">
    <mat-spinner  mode="indeterminate" ></mat-spinner>
  </ng-container>
</section>
  

Y el siguiente código en el componente.ts:


  cargando = false;
  resultado = '';
  printado = false;

  constructor(public api: ConsultarapiService) { }

  ngOnInit(): void {
  }

  descarga_fichero() {
    const auxiliar = this.api.saludo()
    auxiliar.subscribe({
      complete: () => {
        this.cargando = false ;
      },
      error: () => {
        this.printado = true;
        this.cargando = false ;
        this.resultado = 'Error en la comunicación con el servidor';
      },
      next: (resultado: Blob) => {
        const dataType = resultado.type;
        const binaryData = [];
        binaryData.push(resultado);
        const downloadLink = document.createElement('a');
        downloadLink.href = window.URL.createObjectURL(new Blob(binaryData, {type: dataType}));
        downloadLink.setAttribute('download', 'resultado_comprimido.pdf');
        document.body.appendChild(downloadLink);
        downloadLink.click();
        this.resultado = 'Respuesta recibida correctamente. Esta es la respuesta.'
        this.printado = true;
      }
    });
  }