DEV Community

Junior
Junior

Posted on

5 tips to improve integration with the backend, Angular and Rxjs

Introduction

Unlike react, we have many libraries to communicate with the backend as mentioned in this post 5 tips to improve integration with the backend, React with Axios, in angular it is recommended to use the HttpClient and the rxjs that come with it, of course we can still install an axios or use a fetch without problems.

Even though angular has these points, it is important to think of points to help us with maintenance and better communication so as not to affect the usability of the user.

Angular e rxjs

1 - Encapsulate service

We must create a generic service called with the library that we chose to use for the integration and simply just use it in the application, with the same idea of components like card, inputs, among others that we already do.

Serviço

First we have to create an angular service http-request.service.ts (Remembering you can put another name if you want) it will contain all the generic http methods.

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

We created an httpRequest function (already thinking about reuse) that is being used by all generic methods, as we will evolve it in the next topics.

In the end we will have a folder structure like this.

Estrutura de pasta

We will be able to use the service in our components this way.

Following good practices, we created a service in our module where all calls to our components are located.

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

Now just importing this service into our component and passing the data to it.

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

But why is it necessary to use this approach?

Porque devemos usar

When we work with a generic service and only import it into our application, it becomes simpler to maintain and modify later. See how we can do it below.

2 - Add headers to all requests

Now let's configure headers for all our requests, we'll need this point to pass token among other information that your backend may need as a business rule.

Let's create an interceptor file (remembering that you can put another name if you want) for this, as it is the best way to not just repeat code. See the example below.

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

And let's reference that interceptor in our 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

Here we have already retrieved the localstorage token and added it to all calls to the backend.

3 - Redirect unauthorized or unauthenticated user

We must have user redirection strategies when the user does not have authorization or permission so that he does not have the need to do this in our components.

At this point, I'm going to leave the code ready for the redirection and the fourth topic for better use.

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();
      // User redirection rule for login page
      this.router.navigateByUrl('/login');
    }
    if (status === 403) {
      // User redirect rule to not allowed page
      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

Leave open where you want to send the user for both 401(Unauthenticated) and 403(Unauthorized). That way, even if the user manages to access a page that he couldn't, when the backend request comes back with the status code, the system will already direct him, this approach also works for when the token expires, which we'll see how to deal with later.

4 - Request retry pattern

Now we will need to apply a pattern retry to our requests so that our end user does not suffer from instabilities in the application as it may be undergoing a deploy or auto scaling of the infrastructure at the time of the call. For this we define a number of attempts in case the system returns error 500 or higher. Example showing no.

With the help of the rxjs operators we can do a retryWhen as many times as configured in the retryAttempts environment every time the request has a status code greater than 500.

5 - Refresh token

When we work with authentication, it is a good practice and security rule to have tokens that expire so that the user does not stay logged in forever even without using the application.

However, we have to solve the problem that if the token expires when the user cannot simply ask him to log in again, for that we have routes to refresh token.

We can improve our http-request.service.ts service so that it does this automatically when calling routes from your application.

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

We created a refreshToken() function that receives an Observable and returns the same, but it would check if the token's expired has already passed, and if so, make a new call to the backend, renewing the token and the expired. Remembering that this logic works according to the backend and the refresh route, for example, has a time limit after passing from expired to renewing the token, that would be more of a business rule.

Conclusion

In this post we saw five ways to improve our communication with the backend and taking into account the best experience for the end user, there are many other approaches that can improve our backend call service, but just by implementing these concepts we will have better maintenance and usability of our system. In future posts we will see how to further improve this service.

Obrigado até a próxima

To the next

References

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

Top comments (1)

Collapse
 
naucode profile image
Al - Naucode

Hey, that was a nice read, you got my follow, keep writing 😉