Angular: Fancy Api Integration ๐Ÿฅณ

ยท

5 min read

Today I am going to show you a way to organize services that are responsible for your web requests. I am sure, you will be absolutely astonished by the elegancy of this design. ๐ŸŽ‰

What you will get ๐Ÿค“

  • An easy way to add mocking to your application

  • A centralized api module that you can easily share across your applications

  • Clean Code ๐Ÿคฏ

What is mocking?

Mocking means working with fake data, that has the same structure as your live data.

It is especially useful if your project is still in development & the web api is not fully developed. Then you could simply mock the data in the meantime. โœ…

Another use case for mocking would be if you don't have any web api for your test environment. You could then just use mocking to make the application useable in this case. โœ…

Last but not least, if you want to provide your customers with a sample of your application, you could provide them with sample that uses proper mock data in the background. No web api required. โœ…

How do I get started?

Now I will show you how to implement such an api module in your application ๐Ÿ˜Š As a showcase I will implement some parts of the Open Brewery DB ๐Ÿบ

Api Folder

Add a folder named accordingly to your api in a folder of your choice. I like to put application wide used code in the shared folder.

For this showcase I created the folder src/app/shared/brewery-api.

Api Models

Now put all of your models into the folder src/app/shared/brewery-api/models/.

src/app/shared/brewery-api/models/brewery.model.ts:

export interface Brewery {
  id: string;
  name: string;
  brewery_type: string;
  address_1: string;
  address_2: string;
  address_3: string;
  city: string;
  state_province: string;
  postal_code: string;
  country: string;
  longitude: string;
  latitude: string;
  phone: string;
  website_url: string;
  state: string;
  street: string;
}

Api Service Implementations

Next create a file for your api service implementations.

src/app/shared/brewery-api/search/search-api.ts:

// Injection token later used to inject the api into your components, services, ...
export const SEARCH_API = new InjectionToken<SearchApi>('SEARCH_API');

// Interface representing the public api of the services.
export interface SearchApi {
  search(query: string): Observable<Brewery[]>;
}

// Implementation with REAL web requests
@Injectable()
export class SearchApiService implements SearchApi {

  private readonly baseUrl = inject(BASE_URL);
  private readonly httpClient = inject(HttpClient);

  public search(query: string): Observable<Brewery[]> {
    return this.httpClient.get<Brewery[]>(`${this.baseUrl}/breweries/search`, { params: { query } });
  }

}

// Mock implementations
@Injectable()
export class SearchApiMockService implements SearchApi {

  public search(query: string): Observable<Brewery[]> {
    return of([
      {
        id: "0758976d-8fed-46cc-abec-1cb02cbca0d6",
        name: "Some Brewery",
        brewery_type: "proprietor",
        address_1: "291 S 1st St,St.",
        address_2: null,
        address_3: null,
        city: "Saint Helens",
        state_province: "Oregon",
        postal_code: "97051",
        country: "United States",
        longitude: "-122.7980095",
        latitude: "45.86251169",
        phone: "5033971103",
        website_url: "http://www.some-brewery.com",
        state: "Oregon",
        street: "291 S 1st St,St."
      }
    ]);
  }

}

It is a good practice to split those implementations, interfaces & injection tokens into seperate files, which I would recommend if the code gets any longer. ๐Ÿงพ

Furthermore I recommend to bundle the public accessible parts in index.ts files as shown here:

src/app/shared/brewery-api/search/index.ts:

export { SEARCH_API, SearchApi } from './search-api';

This will result in nice & clean imports when you later use the api module in your application. I usually create index files for the models and the api provider (described below) as well.

Api Provider

Finally create a provider file, which will be later used to register the api module in your application.

src/app/shared/brewery-api/brewery-api.provider.ts:

// Injection token used to provide the api services with the base url.
export const BASE_URL = new InjectionToken<string>('BASE_URL');

export interface BreweryApiOptions {
  mock: boolean;
  baseUrlFactory: () => string;
}

export function provideBreweryApi(options: BreweryApiOptions): EnvironmentProviders {
  return makeEnvironmentProviders([
    { provide: SEARCH_API, useClass: options?.mock ? SearchApiMockService : SearchApiService },
    {
      provide: BASE_URL,
      useFactory: options.baseUrlFactory
    }
  ]);
}

Your folder structure should now look somewhat like this: ๐ŸŒณ

src/app/shared/
โ””โ”€โ”€ brewery-api/
    โ”œโ”€โ”€ models/
    โ”‚   โ”œโ”€โ”€ brewery.model.ts
    โ”‚   โ””โ”€โ”€ index.ts
    โ”œโ”€โ”€ search/
    โ”‚   โ”œโ”€โ”€ search-api.ts
    โ”‚   โ””โ”€โ”€ index.ts
    โ”œโ”€โ”€ brewery-api.provider.ts
    โ””โ”€โ”€ index.ts

And... we are done! ๐Ÿฅณ

All left to do is to use it. ๐Ÿ˜‰

Using the api module

Registering the api module

Add the provider function to your app.config.ts or app.module.ts(If your still using the old api):

src/app/app.config.ts:

export const appConfig: ApplicationConfig = {
  providers: [
    // ...
    provideBreweryApi({
      mock: environment.mock,
      baseUrlFactory: () => environment.baseUrl
    })
  ]
};

src/app/app.module.ts:

@NgModule({
  providers: [
    // ...
    provideBreweryApi({
      mock: environment.mock,
      baseUrlFactory: () => environment.baseUrl
    })
  ]
})
export class AppModule {}

At this point you can implement the baseUrlFactory accordingly. In some cases you might have a dependency to some service that is required to construct the base url. Since a provider function is a valid injection context, you can just use the method inject(MyService) to obtain an instance of the required service:

provideBreweryApi({
  mock: environment.mock,
  baseUrlFactory: () => inject(ConfigService).getBaseUrl()
})

Using the api services

With everything wired up you can finally send some http requests to your web api. ๐ŸŽ‰

src/app/pages/search/search.component.ts:

@Component({
  selector: 'app-search',
  standalone: true,
  templateUrl: './search.component.html'
})
export class SearchComponent implements OnInit, OnDestroy {

  private sub: Subscription;

  // Using inject method to obtain an instance of the search api.
  private readonly searchApi = inject(SEARCH_API);

  // You can also use the classic approach as demonstrated below.
  // public constructor(
  //   @Inject(SEARCH_API) private searchApi: SearchApi
  // ) {}

  public ngOnInit(): void {
    // You can now use the methods declared in the SearchApi interface.
    this.sub = this.searchApi.search('Some search string').subscribe({
      next: res => console.log('Got search results :)', res),
      error: e => console.error('Something went wrong :(', e)
    });
  }

  public ngOnDestroy(): void {
    this.sub?.unsubscribe();
  }

}

Stackblitz example

Reading is boring? I know! But hold on and take a look at this live stackblitz example of an api module: Stackblitz Example ๐Ÿคฉ

Further Ideas

NPM package

Often times multiple apps depend on a single web api. In this case you might want to publish the api module as npm package. Than you can just install the api in all of your apps. ๐Ÿ”ฅ

Laaaarge mock data

Sometimes the required mock data can get quite large. In this case you can put your mock data in json files, placed in the assets folder. You can then load this files with the HttpClient.

Wrapping up

Thank you for making it this far! ๐Ÿ˜Š I sincerely hope I could improve your dev skills a little bit & would love to hear your opinion on this approach. Have a great day and see you soon! ๐Ÿ‘‹

ย