SpeedOf.Me API - Angular Integration

This example demonstrates how to integrate the SpeedOf.Me speed test API with Angular.

Prerequisites

Quick Start

  1. Create a new Angular project:
    ng new my-speedtest --standalone
    cd my-speedtest
  2. Add the API script to src/index.html:
    <head>
      <!-- other head content -->
      <script src="https://speedof.me/api/api.js"></script>
    </head>
  3. Create type definitions at src/types/speedofme.d.ts:
    interface SpeedTestResult {
        download: number;
        upload: number;
        latency: number;
        jitter: number;
        testServer?: string;
        ip_address?: string;
        hostname?: string;
    }
    
    interface SpeedTestProgress {
        testType: 'download' | 'upload';
        currentSpeed: number;
        percentComplete: number;
        passNumber: number;
    }
    
    interface SomApi {
        account: string;
        domainName: string;
        config: {
            sustainTime: number;
            testServerEnabled: boolean;
            userInfoEnabled: boolean;
            latencyTestEnabled: boolean;
            uploadTestEnabled: boolean;
        };
        startTest: () => void;
        onTestCompleted: (result: SpeedTestResult) => void;
        onProgress: (progress: SpeedTestProgress) => void;
        onError: (error: { code: number; message: string }) => void;
    }
    
    declare var SomApi: SomApi;
  4. Create the service at src/app/services/speed-test.service.ts:
    import { Injectable, NgZone } from '@angular/core';
    import { BehaviorSubject } from 'rxjs';
    
    export type TestStatus = 'idle' | 'running' | 'completed' | 'error';
    
    @Injectable({ providedIn: 'root' })
    export class SpeedTestService {
        private statusSubject = new BehaviorSubject<TestStatus>('idle');
        private resultSubject = new BehaviorSubject<SpeedTestResult | null>(null);
        private progressSubject = new BehaviorSubject<SpeedTestProgress | null>(null);
        private errorSubject = new BehaviorSubject<{ code: number; message: string } | null>(null);
    
        status$ = this.statusSubject.asObservable();
        result$ = this.resultSubject.asObservable();
        progress$ = this.progressSubject.asObservable();
        error$ = this.errorSubject.asObservable();
    
        constructor(private ngZone: NgZone) {
            this.initializeApi();
        }
    
        private initializeApi(): void {
            SomApi.account = 'YOUR_API_KEY';
            SomApi.domainName = 'your-domain.com';
            SomApi.config.sustainTime = 4;
    
            // NgZone.run ensures Angular change detection works
            SomApi.onTestCompleted = (result) => {
                this.ngZone.run(() => {
                    this.resultSubject.next(result);
                    this.progressSubject.next(null);
                    this.statusSubject.next('completed');
                });
            };
    
            SomApi.onProgress = (progress) => {
                this.ngZone.run(() => {
                    this.progressSubject.next(progress);
                });
            };
    
            SomApi.onError = (error) => {
                this.ngZone.run(() => {
                    this.errorSubject.next(error);
                    this.statusSubject.next('error');
                });
            };
        }
    
        startTest(): void {
            this.statusSubject.next('running');
            this.resultSubject.next(null);
            this.errorSubject.next(null);
            SomApi.startTest();
        }
    }
  5. Create the component at src/app/components/speed-test.component.ts:
    import { Component } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { SpeedTestService } from '../services/speed-test.service';
    
    @Component({
        selector: 'app-speed-test',
        standalone: true,
        imports: [CommonModule],
        template: `
            <div class="speed-test">
                <button
                    (click)="startTest()"
                    [disabled]="(status$ | async) === 'running'"
                >
                    {{ (status$ | async) === 'running' ? 'Testing...' : 'Start Test' }}
                </button>
    
                <div *ngIf="result$ | async as result">
                    <p>Download: {{ result.download }} Mbps</p>
                    <p>Upload: {{ result.upload }} Mbps</p>
                    <p>Latency: {{ result.latency }} ms</p>
                </div>
            </div>
        `
    })
    export class SpeedTestComponent {
        status$ = this.speedTestService.status$;
        result$ = this.speedTestService.result$;
    
        constructor(private speedTestService: SpeedTestService) {}
    
        startTest(): void {
            this.speedTestService.startTest();
        }
    }

Why NgZone?

The SpeedOf.Me API callbacks execute outside Angular's zone, which means change detection won't trigger automatically. Wrapping callback code in ngZone.run() ensures the UI updates properly.

Configuration

SomApi.account = 'YOUR_API_KEY';
SomApi.domainName = 'your-domain.com';
SomApi.config.sustainTime = 4;        // 1-8 seconds
SomApi.config.testServerEnabled = true;
SomApi.config.userInfoEnabled = true;
SomApi.config.latencyTestEnabled = true;
SomApi.config.uploadTestEnabled = true;

Using Environment Variables

Store your API key in environment.ts:

// src/environments/environment.ts
export const environment = {
    production: false,
    speedOfMeApiKey: 'YOUR_API_KEY',
    speedOfMeDomain: 'your-domain.com'
};

Then use in your service:

import { environment } from '../../environments/environment';

SomApi.account = environment.speedOfMeApiKey;
SomApi.domainName = environment.speedOfMeDomain;

RxJS Patterns

The service exposes observables that you can compose with other RxJS operators:

// Combine with other observables
combineLatest([this.status$, this.result$]).pipe(
    filter(([status, result]) => status === 'completed' && result !== null)
).subscribe(([_, result]) => {
    console.log('Test completed:', result);
});

Production Considerations

  1. API Key Security: Use environment files and don't commit keys to version control.
  2. Error Handling: Implement proper error states in your UI.
  3. Loading States: Show skeleton loaders while the API script loads.
  4. NgRx Integration: For larger apps, consider storing test results in NgRx state.

Links