Introduction
In my recently published book, Building an operation-oriented Api using PHP and the Symfony Framework 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 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:
{
"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 to achieve this.
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.
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.
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 service to communicate with the API. Let's see how it looks like:
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.
- It sends the HTTP POST request and returns an HTTP Observable which type will have also been specified by the user (U). It also gets the apiBaseUrl from the Angular environment.
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.
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 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:
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. 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.