Back to Documentation

Angular Integration Guide

Learn how to integrate FXRateSync API with Angular using services, reactive patterns, and TypeScript best practices.

What You'll Learn

  • • Create Angular services for API integration
  • • Implement reactive patterns with RxJS
  • • Build type-safe components with TypeScript
  • • Handle errors and loading states
  • • Create reusable currency components

Prerequisites

This guide assumes you have:

  • Angular 15+ installed
  • Basic knowledge of TypeScript and RxJS
  • FXRateSync API key

Step 1: Create Currency Service

First, create a service to handle all currency-related API calls:

src/app/services/currency.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable, BehaviorSubject, throwError } from 'rxjs';
import { map, catchError, retry, shareReplay } from 'rxjs/operators';
import { environment } from '../../environments/environment';

export interface ConversionResponse {
  from: string;
  to: string;
  amount: number;
  converted_amount: number;
  rate: number;
  timestamp: string;
  source: string;
}

export interface CurrencyRate {
  code: string;
  rate: number;
  name: string;
}

@Injectable({
  providedIn: 'root'
})
export class CurrencyService {
  private readonly API_URL = environment.apiUrl;
  private readonly API_KEY = environment.apiKey;
  
  private loadingSubject = new BehaviorSubject<boolean>(false);
  public loading$ = this.loadingSubject.asObservable();
  
  private errorSubject = new BehaviorSubject<string | null>(null);
  public error$ = this.errorSubject.asObservable();

  constructor(private http: HttpClient) {}

  private getHeaders(): HttpHeaders {
    return new HttpHeaders({
      'Content-Type': 'application/json',
      'X-API-Key': this.API_KEY
    });
  }

  /**
   * Convert amount from one currency to another
   */
  convert(from: string, to: string, amount: number): Observable<ConversionResponse> {
    this.loadingSubject.next(true);
    this.errorSubject.next(null);

    const url = `${this.API_URL}/api/v1/convert`;
    const params = { from, to, amount: amount.toString() };

    return this.http.get<ConversionResponse>(url, {
      headers: this.getHeaders(),
      params
    }).pipe(
      retry(2),
      map(response => ({
        ...response,
        converted_amount: Number(response.converted_amount),
        rate: Number(response.rate)
      })),
      catchError(this.handleError.bind(this)),
      shareReplay(1),
      // Always update loading state
      finalize(() => this.loadingSubject.next(false))
    );
  }

  /**
   * Get latest exchange rates for a base currency
   */
  getLatestRates(baseCurrency: string = 'USD'): Observable<CurrencyRate[]> {
    const url = `${this.API_URL}/api/v1/latest`;
    const params = { base: baseCurrency };

    return this.http.get<{rates: Record<string, number>}>(url, {
      headers: this.getHeaders(),
      params
    }).pipe(
      map(response => 
        Object.entries(response.rates).map(([code, rate]) => ({
          code,
          rate,
          name: this.getCurrencyName(code)
        }))
      ),
      catchError(this.handleError.bind(this)),
      shareReplay(1)
    );
  }

  /**
   * Get supported currencies
   */
  getSupportedCurrencies(): Observable<string[]> {
    const url = `${this.API_URL}/api/v1/currencies`;
    
    return this.http.get<{currencies: {code: string, name: string}[]}>(url, {
      headers: this.getHeaders()
    }).pipe(
      map(response => response.currencies.map(c => c.code)),
      catchError(this.handleError.bind(this)),
      shareReplay(1)
    );
  }

  private handleError(error: HttpErrorResponse): Observable<never> {
    let errorMessage = 'An unexpected error occurred';
    
    if (error.error instanceof ErrorEvent) {
      // Client-side error
      errorMessage = `Error: ${error.error.message}`;
    } else {
      // Server-side error
      switch (error.status) {
        case 400:
          errorMessage = 'Invalid request parameters';
          break;
        case 401:
          errorMessage = 'Invalid API key';
          break;
        case 429:
          errorMessage = 'Rate limit exceeded. Please try again later.';
          break;
        case 500:
          errorMessage = 'Server error. Please try again later.';
          break;
        default:
          errorMessage = `Error Code: ${error.status}
Message: ${error.message}`;
      }
    }
    
    this.errorSubject.next(errorMessage);
    return throwError(() => new Error(errorMessage));
  }

  private getCurrencyName(code: string): string {
    const currencyNames: Record<string, string> = {
      USD: 'US Dollar',
      EUR: 'Euro',
      GBP: 'British Pound',
      JPY: 'Japanese Yen',
      CAD: 'Canadian Dollar',
      AUD: 'Australian Dollar',
      CHF: 'Swiss Franc',
      CNY: 'Chinese Yuan'
    };
    return currencyNames[code] || code;
  }
}

Step 2: Environment Configuration

Configure your API credentials in the environment files:

src/environments/environment.ts
export const environment = {
  production: false,
  apiUrl: 'https://api.fxratesync.io',
  apiKey: 'your_api_key_here'
};
⚠️

Security Note

Never commit API keys to version control. Use environment variables in production.

Step 3: Create Currency Converter Component

Build a reusable component for currency conversion:

src/app/components/currency-converter.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subject, debounceTime, takeUntil, distinctUntilChanged } from 'rxjs';
import { CurrencyService, ConversionResponse } from '../services/currency.service';

@Component({
  selector: 'app-currency-converter',
  templateUrl: './currency-converter.component.html',
  styleUrls: ['./currency-converter.component.scss']
})
export class CurrencyConverterComponent implements OnInit, OnDestroy {
  converterForm!: FormGroup;
  result: ConversionResponse | null = null;
  supportedCurrencies: string[] = [];
  loading$ = this.currencyService.loading$;
  error$ = this.currencyService.error$;
  
  private destroy$ = new Subject<void>();

  constructor(
    private fb: FormBuilder,
    private currencyService: CurrencyService
  ) {}

  ngOnInit(): void {
    this.initializeForm();
    this.loadSupportedCurrencies();
    this.setupAutoConversion();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private initializeForm(): void {
    this.converterForm = this.fb.group({
      amount: [100, [Validators.required, Validators.min(0.01)]],
      fromCurrency: ['USD', Validators.required],
      toCurrency: ['EUR', Validators.required]
    });
  }

  private loadSupportedCurrencies(): void {
    this.currencyService.getSupportedCurrencies()
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (currencies) => {
          this.supportedCurrencies = currencies;
        },
        error: (error) => {
          console.error('Failed to load currencies:', error);
          // Fallback currencies
          this.supportedCurrencies = ['USD', 'EUR', 'GBP', 'JPY', 'CAD', 'AUD'];
        }
      });
  }

  private setupAutoConversion(): void {
    this.converterForm.valueChanges
      .pipe(
        debounceTime(500),
        distinctUntilChanged(),
        takeUntil(this.destroy$)
      )
      .subscribe(() => {
        if (this.converterForm.valid) {
          this.convert();
        }
      });
  }

  convert(): void {
    if (!this.converterForm.valid) return;

    const { amount, fromCurrency, toCurrency } = this.converterForm.value;
    
    this.currencyService.convert(fromCurrency, toCurrency, amount)
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (result) => {
          this.result = result;
        },
        error: (error) => {
          console.error('Conversion failed:', error);
          this.result = null;
        }
      });
  }

  swapCurrencies(): void {
    const fromCurrency = this.converterForm.get('fromCurrency')?.value;
    const toCurrency = this.converterForm.get('toCurrency')?.value;
    
    this.converterForm.patchValue({
      fromCurrency: toCurrency,
      toCurrency: fromCurrency
    });
  }

  formatCurrency(amount: number, currency: string): string {
    try {
      return new Intl.NumberFormat('en-US', {
        style: 'currency',
        currency: currency,
        minimumFractionDigits: 2,
        maximumFractionDigits: 6
      }).format(amount);
    } catch (error) {
      return `${amount.toLocaleString()} ${currency}`;
    }
  }
}

Step 4: Component Template

Create the HTML template for the currency converter:

src/app/components/currency-converter.component.html
<div class="currency-converter">
  <h2 class="text-2xl font-bold mb-6">Currency Converter</h2>
  
  <form [formGroup]="converterForm" class="space-y-4">
    <!-- Amount Input -->
    <div>
      <label class="block text-sm font-medium text-foreground mb-1">Amount</label>
      <input
        type="number"
        formControlName="amount"
        class="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
        placeholder="Enter amount"
        step="0.01"
        min="0.01"
      />
      <div *ngIf="converterForm.get('amount')?.invalid && converterForm.get('amount')?.touched" 
           class="text-destructive text-sm mt-1">
        Please enter a valid amount
      </div>
    </div>

    <!-- Currency Selection -->
    <div class="grid grid-cols-2 gap-4">
      <div>
        <label class="block text-sm font-medium text-foreground mb-1">From</label>
        <select
          formControlName="fromCurrency"
          class="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
        >
          <option *ngFor="let currency of supportedCurrencies" [value]="currency">
            {{ currency }}
          </option>
        </select>
      </div>
      
      <div>
        <label class="block text-sm font-medium text-foreground mb-1">To</label>
        <select
          formControlName="toCurrency"
          class="w-full px-3 py-2 border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
        >
          <option *ngFor="let currency of supportedCurrencies" [value]="currency">
            {{ currency }}
          </option>
        </select>
      </div>
    </div>

    <!-- Swap Button -->
    <div class="flex justify-center">
      <button
        type="button"
        (click)="swapCurrencies()"
        class="px-4 py-2 bg-muted hover:bg-muted rounded-md transition-colors"
      >
        ⇄ Swap
      </button>
    </div>

    <!-- Convert Button -->
    <button
      type="button"
      (click)="convert()"
      [disabled]="!converterForm.valid || (loading$ | async)"
      class="w-full px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:bg-muted transition-colors"
    >
      <span *ngIf="loading$ | async">Converting...</span>
      <span *ngIf="!(loading$ | async)">Convert</span>
    </button>
  </form>

  <!-- Loading State -->
  <div *ngIf="loading$ | async" class="mt-4 text-center">
    <div class="inline-block animate-spin rounded-full h-6 w-6 border-b-2 border-primary"></div>
    <span class="ml-2 text-muted-foreground">Converting...</span>
  </div>

  <!-- Error State -->
  <div *ngIf="error$ | async as error" class="mt-4 p-4 bg-destructive/10 border border-destructive/30 rounded-md">
    <p class="text-destructive-foreground">{{ error }}</p>
  </div>

  <!-- Result -->
  <div *ngIf="result && !(loading$ | async)" class="mt-6 p-4 bg-success/10 border border-success/30 rounded-md">
    <h3 class="text-lg font-semibold text-success-foreground mb-2">Conversion Result</h3>
    <div class="text-2xl font-bold text-success-foreground mb-2">
      {{ formatCurrency(result.converted_amount, result.to) }}
    </div>
    <div class="text-sm text-success space-y-1">
      <p>{{ formatCurrency(result.amount, result.from) }} = {{ formatCurrency(result.converted_amount, result.to) }}</p>
      <p>Exchange Rate: 1 {{ result.from }} = {{ result.rate }} {{ result.to }}</p>
      <p>Last Updated: {{ result.timestamp | date:'medium' }}</p>
      <p>Source: {{ result.source }}</p>
    </div>
  </div>
</div>

Step 5: Module Configuration

Update your module to include the necessary imports:

src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

import { AppComponent } from './app.component';
import { CurrencyConverterComponent } from './components/currency-converter.component';

@NgModule({
  declarations: [
    AppComponent,
    CurrencyConverterComponent
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule,
    HttpClientModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Advanced Features

Currency Rate Display Component

Create a component to display real-time currency rates:

src/app/components/currency-rates.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Subject, interval, switchMap, takeUntil } from 'rxjs';
import { CurrencyService, CurrencyRate } from '../services/currency.service';

@Component({
  selector: 'app-currency-rates',
  template: `
    <div class="currency-rates">
      <h3 class="text-lg font-semibold mb-4">Live Exchange Rates</h3>
      <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        <div *ngFor="let rate of rates" class="p-4 bg-background rounded-lg shadow">
          <div class="flex justify-between items-center">
            <span class="font-medium">{{ rate.code }}</span>
            <span class="text-lg font-bold">{{ rate.rate | number:'1.2-6' }}</span>
          </div>
          <div class="text-sm text-muted-foreground">{{ rate.name }}</div>
        </div>
      </div>
    </div>
  `
})
export class CurrencyRatesComponent implements OnInit, OnDestroy {
  rates: CurrencyRate[] = [];
  private destroy$ = new Subject<void>();

  constructor(private currencyService: CurrencyService) {}

  ngOnInit(): void {
    // Load rates immediately and then every 60 seconds
    this.loadRates();
    interval(60000)
      .pipe(
        switchMap(() => this.currencyService.getLatestRates()),
        takeUntil(this.destroy$)
      )
      .subscribe(rates => this.rates = rates);
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }

  private loadRates(): void {
    this.currencyService.getLatestRates()
      .pipe(takeUntil(this.destroy$))
      .subscribe(rates => this.rates = rates);
  }
}

Best Practices

  • Use Services: Centralize API logic in services for reusability
  • Reactive Programming: Leverage RxJS for handling async operations
  • Error Handling: Implement comprehensive error handling strategies
  • Type Safety: Use TypeScript interfaces for API responses
  • Performance: Use shareReplay() to cache API responses
  • Memory Management: Always unsubscribe to prevent memory leaks

Testing

Example unit test for the currency service:

src/app/services/currency.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { CurrencyService } from './currency.service';

describe('CurrencyService', () => {
  let service: CurrencyService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule]
    });
    service = TestBed.inject(CurrencyService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    httpMock.verify();
  });

  it('should convert currency successfully', () => {
    const mockResponse = {
      from: 'USD',
      to: 'EUR',
      amount: 100,
      converted_amount: 88.17,
      rate: 0.8817,
      timestamp: '2024-01-15T12:00:00Z',
      source: 'ecb'
    };

    service.convert('USD', 'EUR', 100).subscribe(result => {
      expect(result).toEqual(mockResponse);
    });

    const req = httpMock.expectOne(req => req.url.includes('/api/v1/convert'));
    expect(req.request.method).toBe('GET');
    req.flush(mockResponse);
  });
});

Complete Angular Example

Want to see a complete Angular application with FXRateSync?