Optimized Service/ API Integration for Angular
Part four of the opinionated guide for well structured applications using Bootstrap, SCSS and Angular Material.
Before starting with this part of the guide, it will be necessary to complete the part(s) that precede it —
Service/ API Integration
By the time we reach this step, all the components have been styled and the application is ready to be transformed from being a static to a dynamic one by integrating with backend API. In order to work with the API and maintain the state of the application, Angular Services will also be included.
Adding the ‘dictionary’ service
This service will contain the read-only configuration data that is to be used across the application. The read-only configuration will contain API URLs, error messages, regexes and other global configuration/data/flags. This is the only service in the application which will not maintain the state of the application.
We will add this service using the following command —
ng g s app-dictionary --skip-tests
Note: We will not be generating the spec file for this service as there is nothing substantial to test. For other services, you can remove the
Adding the global utility service
This service will extend the app-dictionary.service.ts mentioned above. Hence, it will inherit all the application configuration from its parent.
Apart from having the configuration, it will contain all the utility functions that are needed across the application and will also be responsible for maintaining the global application state.
E.g. One of the major utilities to be included is the serviceWrapper function which wraps all the service calls and is a single point in the application through which all calls are made.
Adding the module specific services
These services will be placed inside the module directories, one for each of the feature modules. Each of the service will store the state for that specific module along with the processing of the APIs and any other utility functions that are limited to that particular feature module (for utilities that are not limited to any specific module, we will be defining them in the global utility service, as mentioned in the previous section).
Non-Redux state management
As we are not using Redux, the state management will be done in two levels.
- Global application state will be stored in app-utlity.service.ts and will be updated and accessed only through the methods specified —
2. Module state will be stored in the module specific service file which can be updated and accessed only through the methods specified —
Adding an interceptor
We will also add an interceptor that will come handy for any subsequent request/response transformation that might be required. This way, a common logic can be written for all the HTTP calls. Eg. We will need to allow credentials for secure API calls.
For generating the interceptor, we will use the following command —
ng g interceptor app
This will generate the interceptor file in the same level as the app.module.ts file.
Next, we will add the interceptor to the providers array in app.module.ts for it to take effect —
Keeping placeholder JSONs for API
In case we do not have any dummy backend API ready to connect to, we can keep the dummy JSONs in our codebase and mock the service calls. We will be keeping the dummy JSONs in /assets/JSONs directory —
Calling an API
With the framework now set, we can make a service call and check if everything is working as expected. For an example, we will integrate the login service call. The following changes are to be done for this (and every other API call )—
- Having the API/JSON file ready and the API URL added in the dictionary file.
2. In the appropriate service (either feature module service or the global utility service in this example), we will write a method that calls the serviceWrapper() method in the global utilities and processes the response —
Note: We will process the API response in the service layer itself. The bulk of the business logic processing will be done in this layer and only the processed data/error is left for the next and the final layer i.e. the component from where the service call was initiated.
3. Finally, we will call the method created above, in the service layer, from the component which needs the data. We will also make sure to unsubscribe from the service call subscriptions when the component is destroyed, to avoid any nasty side effect. We can create a utility method to unsubscribe from a list of subscriptions.
Adding a loader for API calls
How loaders are to be used is essentially a decision for UX but for our sample application, we will be having a common one for all the service calls so that the handling is done automatically, using the following steps —
- Adding the loader markup in app component and styling it.
2. Adding the logic to optimize the show/hide behavior based on the ongoing calls in app component.
3. Adding the loader logic in the serviceWrapper() function in global utility service so that it is taken care of automatically for all service calls.
With all the steps completed as per the guide, we will have the codebase ready for developing our application. We will keep adding more pages/ components, modules and their respective services. We can also add route guards, validators and other goodies. The codebase/naming structure will be intuitive for all such new components —
- If it is something common across the application, we will keep it in the same level as app module and name them as
E.g. app.interceptor.ts, app.validators.ts, app-utility.service.ts
- If it is specific to a given module, we will keep it inside the module directory and name them as
P.S. You can refer to my Git repo for all the code and the latest updates on the sample application.
You can visit https://angular.owrrpon.dev/ for the working sample Angular application that follows the optimized codebase setup outlined in the above guide.