DEV Community

Junior
Junior

Posted on • Updated on

5 dicas para melhorar integração com o backend, Angular e Rxjs

Introdução

Diferente do react que temos muitas bibliotecas para realizar comunicação com o backend como foi mencionado nesse post 5 dicas para melhorar integração com o backend, React com Axios, no angular é recomendado utilizar o HttpClient e o rxjs que já vem com ele, isso é claro que ainda podemos instalar um axios ou usar um fetch sem problemas.

Mesmo que no angular tenha esses pontos é importante pensar em pontos para nos ajudar na manutenção e uma melhor comunicação para não afetar a usabilidade do usuário.

Angular e rxjs

1 - Encapsular serviço

Devemos criar um serviço genérico de chamada com a biblioteca que escolhemos usar para fazer a integração e simplesmente só utilizar o mesmo na aplicação, com um tudo a mesma ideia de componentes como card, inputs, entre outros que já fazemos.

Serviço

Primeiro temos que criar um serviço do angular http-request.service.ts (Lembrando você pode colocar outro nome caso queira) ele irá conter todos os métodos http genéricos.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class HttpRequestService {
  private uri = `${environment.url}`;

  constructor(private http: HttpClient, private router: Router) {}

  private httpRequest<Data>(method: string, url: string, options: any): Observable<any> {
    return this.http.request(method, `${this.uri}${url}`, { ...options }).pipe(take(1));
  }

  post<Data>(url: string, body: any): Observable<any> {
    return this.httpRequest('post', url, { body: body });
  }

  get<Data>(url: string, params?: any): Observable<any> {
    return this.httpRequest('get', url, { params: params });
  }

  put<Data>(url: string, body: any): Observable<any> {
    return this.httpRequest('put', url, { body: body });
  }

  delete<Data>(url: string): Observable<any> {
    return this.httpRequest('delete', url, {});
  }
}
Enter fullscreen mode Exit fullscreen mode

Criamos uma função httpRequest (já pensando em reuso) que está sendo usada por todos os métodos genéricos, pois vamos evoluir ela nos próximo tópicos.

No final teremos uma estrutura de pasta assim.

Estrutura de pasta

Vamos poder usar o serviço em nossos componentes dessa maneira.

Seguindo as boas práticas criamos um serviço em nosso em nosso módulo onde ficar todas as chamadas de nosso componentes.

import { Injectable } from '@angular/core';
import { HttpRequestService } from 'src/app/shared/http/http-request.service';

import { Authenticate } from '../model/authenticate';

@Injectable({
  providedIn: 'root'
})
export class AuthService {
  constructor(private httpRequest: HttpRequestService) {}

  login(user: Authenticate) {
    return this.httpRequest.post(`/authenticate`, user);
  }
}

Enter fullscreen mode Exit fullscreen mode

Agora só importando esse serviço em nosso componente e passando os dados para o mesmo.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

import { AuthService } from '../service/auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.scss']
})
export class LoginComponent implements OnInit {
  public loginForm!: FormGroup;
  public loading = false;
  public error = '';

  constructor(
    private _formBuilder: FormBuilder,
    private _authService: AuthService,
  ) {}

  ngOnInit(): void {
    this.loginForm = this._formBuilder.group({
      email: ['', [Validators.required, Validators.email]],
      password: ['', [Validators.required]]
    });
  }

  handleLogin() {
    this.loading = true;
    this._authService.login(this.loginForm.value).subscribe({
    next: data => {
        this.loginForm.reset();
        this.loading = false;
    },
    error: ({ error }) => {
        this.loading = false;
    }
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Mas porque se faz necessário utilizar essa abordagem?

Porque devemos usar

Quando trabalhamos com serviço genérico e só importamos ele em nossa aplicação fica mais simples de realizar manutenção e de modificar posteriormente. Veja como podemos fazer abaixo.

2 - Adicionar headers a todas request

Agora vamos configurar headers a todas nossas request, vamos necessitar desse ponto para passar token entre outras informações que seu backend pode precisar como regra de negócio.

Vamos criar um arquivo interceptor (Lembrando você pode colocar outro nome caso queira) para isso, pois é a melhor maneira para não ficarmos só repetindo código. Veja o exemplo abaixo.

import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';

interface Header {[key: string]: string}

@Injectable()
export class Intercept implements HttpInterceptor {
  intercept(httpRequest: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    if (httpRequest.headers.get('noHeader')) {
      const cloneReq = httpRequest.clone({
        headers: httpRequest.headers.delete('noHeader')
      });
      return next.handle(cloneReq);
    }

    const headers: Header = {
      Authorization: `Bearer ${localStorage.getItem('token') || ''}`
    };

    return next.handle(httpRequest.clone({ setHeaders: { ...headers } }));
  }
}
Enter fullscreen mode Exit fullscreen mode

E vamos referência esse interceptor em nosso appModule.

import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './layouts/app/app.component';
import { Intercept } from './shared/http/intercept';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    HttpClientModule,
    AppRoutingModule,
    BrowserAnimationsModule
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: Intercept,
      multi: true
    },
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Aqui já recuperamos o token do localstorage e adicionamos a todas as chamadas ao backend.

3 - Redirecionamento usuário não autorizado ou não autenticado

Devemos ter estratégias de redirecionamento de usuário quando o mesmo não tiver autorização ou permissão para que não tenha a necessidade de fazer isso em nossos componentes.

Nesse ponto já vou deixar o código pronto do redirecionamento e do quarto tópico para melhor aproveitamento.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { retryWhen, scan, take } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

@Injectable({
  providedIn: 'root'
})
export class HttpRequestService {
  private uri = `${environment.url}`;

  constructor(private http: HttpClient, private router: Router) {}

  private redirectUser(status: number): void {
    if (status === 401) {
      localStorage.clear();
      // Regra de redirecionamento de usuário para página de login
      this.router.navigateByUrl('/login');
    }
    if (status === 403) {
      // Regra de redirecionamento de usuário para página de não permitido
      this.router.navigateByUrl('/');
    }
  }

  private httpRequest<Data>(method: string, url: string, options: any): Observable<any> {
    return this.http.request(method, `${this.uri}${url}`, { ...options }).pipe(
      retryWhen(e =>
        e.pipe(
          scan((errorCount, error) => {
            this.redirectUser(error.status);
            if (errorCount >= environment.retryAttempts || error.status < 500) throw error;
            return errorCount + 1;
          }, 0)
        )
      ),
      take(1)
    );
  }

  post<Data>(url: string, body: any): Observable<any> {
    return this.httpRequest('post', url, { body: body });
  }

  get<Data>(url: string, params?: any): Observable<any> {
    return this.httpRequest('get', url, { params: params });
  }

  put<Data>(url: string, body: any): Observable<any> {
    return this.httpRequest('put', url, { body: body });
  }

  delete<Data>(url: string): Observable<any> {
    return this.httpRequest('delete', url, {});
  }
}
Enter fullscreen mode Exit fullscreen mode

Deixe aberto para onde você deseja mandar o usuário tanto para 401(Não autenticado) e 403 (Não autorizado). Dessa maneira mesmo que o usuário conseguir acessar uma página que não poderia quando a request do backend voltar com o status code o sistema já irá direcioná-lo, essa abordagem serve também para quando token expira, que vamos ver como lidar com isso mais adiante.

4 - Pattern retry de request

Agora vamos precisar aplicar ​​pattern retry em nossas requests para que o nosso usuário final não sofra com instabilidades na aplicação pois a mesma pode está passando por um deploy ou auto scaling da infraestrutura no momento da chamada. Para isso nós definimos um número de tentativas caso o sistema retorne erro 500 ou superior. Exemplo mostrando no.

Com o auxílio do operators do rxjs podemos fazer um ​​retryWhen quantas vezes for configurado na environment retryAttempts toda vez que a request tenha status code maior igual a 500.

5 - Refresh token

Quando trabalhamos com autenticação é uma boa prática e regra de segurança termos tokens que expiram para que o usuário não fique logado para sempre mesmo sem usar a aplicação.

Porém temos que resolver o problema que se o token expirado quando o usuário não podemos simplesmente pedir para que o mesmo realize login novamente, para isso temos rotas para refresh token.

Podemos melhorar nosso serviço http-request.service.ts para que ele faça isso automaticamente durante as chamadas das rotas da sua aplicação.

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs';
import { retryWhen, scan, take } from 'rxjs/operators';
import { environment } from 'src/environments/environment';

interface Refresh {
  token: string;
  expired: number;
}

@Injectable({
  providedIn: 'root'
})
export class HttpRequestService {
  private uri = `${environment.url}`;

  constructor(private http: HttpClient, private router: Router) {}

  private refreshToken(httpRequest: Observable<any>): Observable<any> {
    const value: number = Number(localStorage.getItem('expired'));
    if (value && new Date(value) < new Date()) {
      this.refresh().subscribe(data => {
        localStorage.setItem('token', data.token);
        localStorage.setItem('expired', String(new Date().setSeconds(data.expired)));
        return httpRequest;
      });
    }
    return httpRequest;
  }

  private refresh(): Observable<Refresh> {
    return this.http.get<Refresh>(`${environment.url}/refresh `).pipe(take(1));
  }

  private notAuthorization(status: number): void {
    if (status === 401) {
      localStorage.clear();
      this.router.navigateByUrl('/login');
    }
  }

  private httpRequest<Data>(method: string, url: string, options: any): Observable<any> {
    return this.http.request(method, `${this.uri}${url}`, { ...options }).pipe(
      retryWhen(e =>
        e.pipe(
          scan((errorCount, error) => {
            this.notAuthorization(error.status);
            if (errorCount >= environment.retryAttempts || error.status < 500) throw error;
            return errorCount + 1;
          }, 0)
        )
      ),
      take(1)
    );
  }

  post<Data>(url: string, body: any): Observable<any> {
    return this.refreshToken(this.httpRequest('post', url, { body: body }));
  }

  get<Data>(url: string, params?: any): Observable<any> {
    return this.refreshToken(this.httpRequest('get', url, { params: params }));
  }

  put<Data>(url: string, body: any): Observable<any> {
    return this.refreshToken(this.httpRequest('put', url, { body: body }));
  }

  delete<Data>(url: string): Observable<any> {
    return this.refreshToken(this.httpRequest('delete', url, {}));
  }
}
Enter fullscreen mode Exit fullscreen mode

Criamos uma função de refreshToken() que recebe um Observable e retorna o mesmo porém ela iria verificar se expired do token já passou, e se sim já realizar uma nova chamada ao backend renovando o token e o expired. Lembrando essa lógica funciona de acordo com o backend e a rota de refresh por exemplo tem um tempo limite depois que passar do expired para se renovar o token ai seria mais regra de negócio.

Conclusão

Nesse post vimos cinco maneiras de melhorar nossa comunicação com o backend e levando em conta a melhor experiência para o usuário final, existem muitas outras abordagens que podem melhorar o nosso serviço de chamada ao backend, porém só em implementar esses conceitos já teremos uma melhor manutenção e usabilidade do nosso sistema. Em posts futuros veremos como melhorar ainda mais esse serviço.

Obrigado até a próxima

Até a próxima

Referências

Angular - https://angular.io/
Rxjs - https://rxjs.dev

Oldest comments (0)