An Angular approach to communicate with an operation-oriented API
Tags: angular, api, operations, typescript
Published: 2024-06-25 16:00:21

## Introduction
In my recently published book, [Building an operation-oriented Api using PHP and the Symfony Framework](https://amzn.eu/d/3eO1DDi) I try to explain step-by-step how to build an API focused on operations using many symfony features such as tagged iterators, service configurators, firewalls, voters, symfony messenger etc.
But, although the book is focused on backend development, It would have been interesting to include a chapter on how to communicate with the API from a frontend application. That's why I've been decided to create this post in which I will show how to prepare an angular application to access to an operation-oriented api. 

> You can also read [this article](https://dev.to/icolomina/an-operation-oriented-api-using-php-and-symfony-4p6d) to see a description about the book.

## The models

### The ApiInput model

An operation-oriented api receives the operation to execute as an HTTP POST request and the operation to perform and the required data comes within the request payload. Bellow you can see an example:

```json
{
    "operation" : "SendPayment",
    "data" : {
        "receiver" : "yyyy"
        "amount" : 21.69
    }
 }
```

As data can vary between operations, we will need an ApiInput interface whose operation data are variable depending on the operation to be executed. Let's rely on [typescript generics](https://www.typescriptlang.org/docs/handbook/2/generics.html) to achieve this.

```typescript
export interface ApiInput<T> {
  name: string,
  data: T
}
```
The **ApiInput** requires a type for the data parameter (T). The name parameter is a string which represents the operation to perform.

### The Operation model
As we have said, each operation has its own parameters so each operation would require its own model. Let's create an type for the **SendPayment** operation.

```typescript
export type SendPaymentInputData = {
  receiver: string,
  amount: number
}
```
The above type, represents the data required to perform a **SendPayment** operation. It contains two parameters, the payment receiver and the amount to send.

### The Operation Output
As with the operation inputs, each operation output can be different so we would also need an output for each operation. Let's create a type for the SendPayment operation output.

```typescript
export type SendPaymentOutputData = {
  id: string
}
```
The above output contains one parameter which would represent the payment identifier.

## The Operation service
The payment service will use the [Angular HttpClient](https://angular.dev/guide/http) service to communicate with the API. Let's see how it looks like:

```typescript
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { ApiInput } from '../model/ApiInput';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class OperationService {

  constructor(private httpClient: HttpClient) { }

  sendOperation<T, U>(data: T, name: string): Observable<U> {
    const apiInput: ApiInput<T> = {
      name: name,
      data: data
    };

    const options = {
      headers: new HttpHeaders({
        'Content-Type':  'application/json',
        Authorization: environment.apiKey
      })
    };

    return this.httpClient.post<U>(environment.apiBaseUrl + '/api/v1/operation', apiInput, options);
  }
}
```
Let's explain it step-by-step:

- The constructor injects the HttpClient service so we can use it.
- It accepts the operation data and the operation name as a parameters. The first parameter type must be defined by the user (T) depending on the operation to perform. 
- It creates an **ApiInput** interface and uses the method specified data type (T) as a type for the **ApiInput** data parameter.
- It creates an options object with the required HTTP headers. It gets the **apiKey** from the [Angular environment](https://angular.dev/tools/cli/environments).
- It sends the HTTP POST request and returns an [HTTP Observable](https://angular.dev/guide/http/making-requests#http-observables) which type will have also been specified by the user (U). It also gets the **apiBaseUrl** from the [Angular environment](https://angular.dev/tools/cli/environments).

## The component

So far, we have defined the models and the service which sends the operation requests. Now we need to call this service to send a request. Let's create a **PaymentComponent** to send payment operation requests.

```typescript
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { SendPaymentInputData, SendPaymentOutputData } from 'src/app/model/ApiInput';
import { PaymentServiceService } from 'src/app/service/payment-service.service';

@Component({
  selector: 'app-payment',
  templateUrl: './payment.component.html',
  styleUrls: ['./payment.component.css']
})
export class PaymentComponent {

  form: FormGroup;

  constructor(private fb: FormBuilder, private operationService: PaymentServiceService) {
    this.form = this.fb.group({
      receiver: [null, Validators.required],
      amount: [null, [Validators.required, Validators.min(0.1)]],
    });
  }

  sendPayment(): void {

    if(this.form.invalid) {
      // logic to show errors
      return;
    }

    const data: SendPaymentInputData = this.form.value as SendPaymentInputData;
    this.operationService.sendOperation<SendPaymentInputData, SendPaymentOutputData>(data, 'SendPayment').subscribe(
      (output: SendPaymentOutputData) => {
          // logic to show the operation result
      }
    )
  }

}
```
Let's analyze the component step-by-step:
- The constructor injects the **FormBuilder** service and creates a form with two fields:
   - **receiver**: The payment receiver
   - **amount**: The amount to pay
- Both receiver and amount parameter are required and amount must be greater than 0. 

> The API validates the data before executing the operation but it's also useful to validate it before sending the request so that we can avoid getting errors from server.

- The **sendPayment** method does not continue if the form data is invalid.
- If the form data is valid, the method uses the typescript assertion "as" to transform the form data object to a **SendPaymentInputData** type. 
- Finally, it uses the **OperationService** **sendOperation** method to request a **SendPayment** operation. As you can see, we specify the input data type as **SendPaymentInputData** and the Observable return type as **SendPaymentOutputData**.

## Handling errors
A common way to handle errors using Angular is to create a response interceptor. [Angular interceptors](https://angular.dev/guide/http/interceptors) are services which allow developers to intercept HTTP requests and execute some logic before or after the request.

In our case, we could create an interceptor to intercept the operation request response to handle errors. Let's see how the interceptor would look like:

```typescript
import { Injectable } from '@angular/core';
import {
  HttpRequest,
  HttpHandler,
  HttpEvent,
  HttpInterceptor,
  HttpErrorResponse
} from '@angular/common/http';
import { Observable, catchError, throwError } from 'rxjs';
import { Router } from '@angular/router';

@Injectable()
export class OperationInterceptor implements HttpInterceptor {

  constructor() {}

  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    return next.handle(request).pipe(
      catchError((e: HttpErrorResponse) => {
        switch(e.status) {
          case 401:
          case 403:
            // Handle autentication and authorization errors
            // In 401 case, You could redirect the user to the login page
            break;
          default:
            // Handle other errors
            break;

        }

        return throwError(() => e);
      })
    );
  }
}
```
The interceptor service implements the **HttpInterceptor** interface and have to define the **intercept** method. The **intercept** method will have the logic to handle errors.
In this case, the **catchError** function is reached when a HttpErrorResponse occurs. Here, we could handle an error depending on the HTTP error status.

## Conclusion

In this post, I have briefly shown how we could prepare an angular application to communicate with an operation-oriented API like the one I propose in [my book](https://amzn.eu/d/3eO1DDi). As the frontend must sent requests to the same endpoint but with different payloads, I have chosen to create a service with a method (**sendOperation**) which uses typescript generics to allow developers to specify the operation data type and the operation output type.