Taking Screenshots in Angular with a Screenshot API

2026-05-09 | Tags: [angular, typescript, api, screenshot, frontend]

Angular's opinionated architecture — services, dependency injection, RxJS observables — maps cleanly onto API integration patterns. If you're building an Angular app that needs to capture web page screenshots, a screenshot API gives you the capability without managing headless browser infrastructure. This guide covers everything from a basic service to production-grade patterns with NgRx and Angular Universal.

Basic Setup

Install nothing new — Angular's HttpClient handles API calls out of the box. You'll need HttpClientModule in your app module (or provideHttpClient() in standalone apps).

Add your API key to environment.ts:

// src/environments/environment.ts
export const environment = {
  production: false,
  screenshotApiKey: 'your_api_key_here',
  screenshotApiUrl: 'https://hermesforge.dev/api/screenshot',
};
// src/environments/environment.prod.ts
export const environment = {
  production: true,
  screenshotApiKey: '', // set via CI/CD environment variable injection
  screenshotApiUrl: 'https://hermesforge.dev/api/screenshot',
};

Screenshot Service

Encapsulate the API call in a dedicated service:

// src/app/services/screenshot.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, map } from 'rxjs';
import { environment } from '../../environments/environment';

export interface ScreenshotOptions {
  url: string;
  width?: number;
  height?: number;
  format?: 'webp' | 'png' | 'jpeg';
  fullPage?: boolean;
  delay?: number;
  js?: string;
}

@Injectable({ providedIn: 'root' })
export class ScreenshotService {
  private readonly apiUrl = environment.screenshotApiUrl;

  constructor(private http: HttpClient) {}

  capture(options: ScreenshotOptions): Observable<string> {
    let params = new HttpParams()
      .set('url', options.url)
      .set('width', String(options.width ?? 1280))
      .set('height', String(options.height ?? 800))
      .set('format', options.format ?? 'webp')
      .set('full_page', String(options.fullPage ?? false));

    if (options.delay) params = params.set('delay', String(options.delay));
    if (options.js) params = params.set('js', options.js);

    return this.http
      .get(this.apiUrl, {
        params,
        responseType: 'blob',
        headers: { 'X-API-Key': environment.screenshotApiKey },
      })
      .pipe(map((blob) => URL.createObjectURL(blob)));
  }
}

Using the Service in a Component

// src/app/components/screenshot-tool/screenshot-tool.component.ts
import { Component, OnDestroy } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { Subject, takeUntil } from 'rxjs';
import { ScreenshotService } from '../../services/screenshot.service';

@Component({
  selector: 'app-screenshot-tool',
  template: `
    <form [formGroup]="form" (ngSubmit)="capture()">
      <input
        formControlName="url"
        placeholder="https://example.com"
        type="url"
      />
      <select formControlName="width">
        <option [value]="1280">Desktop (1280px)</option>
        <option [value]="768">Tablet (768px)</option>
        <option [value]="390">Mobile (390px)</option>
      </select>
      <button type="submit" [disabled]="form.invalid || loading">
        {{ loading ? 'Capturing...' : 'Take Screenshot' }}
      </button>
    </form>

    <img *ngIf="screenshotUrl" [src]="screenshotUrl" alt="Screenshot" />
    <p *ngIf="error" class="error">{{ error }}</p>
  `,
})
export class ScreenshotToolComponent implements OnDestroy {
  form: FormGroup;
  screenshotUrl: string | null = null;
  loading = false;
  error: string | null = null;

  private destroy$ = new Subject<void>();
  private previousUrl: string | null = null;

  constructor(
    private fb: FormBuilder,
    private screenshotService: ScreenshotService
  ) {
    this.form = this.fb.group({
      url: ['', [Validators.required, Validators.pattern('https?://.+')]],
      width: [1280],
    });
  }

  capture(): void {
    if (this.form.invalid) return;

    this.loading = true;
    this.error = null;

    // Clean up previous blob URL
    if (this.previousUrl) {
      URL.revokeObjectURL(this.previousUrl);
      this.previousUrl = null;
    }

    const { url, width } = this.form.value;

    this.screenshotService
      .capture({ url, width })
      .pipe(takeUntil(this.destroy$))
      .subscribe({
        next: (blobUrl) => {
          this.screenshotUrl = blobUrl;
          this.previousUrl = blobUrl;
          this.loading = false;
        },
        error: (err) => {
          this.error = err.message ?? 'Screenshot failed';
          this.loading = false;
        },
      });
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    if (this.previousUrl) URL.revokeObjectURL(this.previousUrl);
  }
}

Note the takeUntil(this.destroy$) pattern — this prevents memory leaks by cancelling in-flight requests when the component is destroyed.

HTTP Interceptor for Auth

Instead of passing the API key in every service call, use an interceptor:

// src/app/interceptors/screenshot-auth.interceptor.ts
import { Injectable } from '@angular/core';
import {
  HttpInterceptor,
  HttpRequest,
  HttpHandler,
} from '@angular/common/http';
import { environment } from '../../environments/environment';

@Injectable()
export class ScreenshotAuthInterceptor implements HttpInterceptor {
  intercept(req: HttpRequest<unknown>, next: HttpHandler) {
    if (req.url.startsWith(environment.screenshotApiUrl)) {
      const authReq = req.clone({
        setHeaders: { 'X-API-Key': environment.screenshotApiKey },
      });
      return next.handle(authReq);
    }
    return next.handle(req);
  }
}

Register in AppModule:

providers: [
  {
    provide: HTTP_INTERCEPTORS,
    useClass: ScreenshotAuthInterceptor,
    multi: true,
  },
],

Now ScreenshotService doesn't need to set the header manually:

// Simplified — interceptor handles auth
return this.http.get(this.apiUrl, { params, responseType: 'blob' }).pipe(
  map((blob) => URL.createObjectURL(blob))
);

RxJS Patterns: Debounce and Retry

For a live preview that captures screenshots as the user types:

import { debounceTime, distinctUntilChanged, switchMap, retry, catchError } from 'rxjs/operators';
import { EMPTY } from 'rxjs';

// In component constructor or ngOnInit:
this.form.get('url')!.valueChanges.pipe(
  debounceTime(1000),         // Wait 1s after last keystroke
  distinctUntilChanged(),      // Skip if URL unchanged
  filter((url) => url?.startsWith('http')),
  switchMap((url) =>           // Cancel previous request on new input
    this.screenshotService.capture({ url }).pipe(
      retry({ count: 2, delay: 1000 }),  // Retry twice on failure
      catchError(() => EMPTY)            // Swallow errors silently
    )
  ),
  takeUntil(this.destroy$)
).subscribe((blobUrl) => {
  this.screenshotUrl = blobUrl;
});

switchMap is the key operator here — it cancels any in-flight screenshot request when a new URL is typed, preventing race conditions where an older request completes after a newer one.

NgRx State Management

For larger apps, manage screenshot state in NgRx:

// src/app/store/screenshot.actions.ts
import { createActionGroup, props, emptyProps } from '@ngrx/store';

export const ScreenshotActions = createActionGroup({
  source: 'Screenshot',
  events: {
    Capture: props<{ url: string; width: number }>(),
    CaptureSuccess: props<{ blobUrl: string }>(),
    CaptureFailure: props<{ error: string }>(),
  },
});
// src/app/store/screenshot.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, switchMap } from 'rxjs/operators';
import { of } from 'rxjs';
import { ScreenshotActions } from './screenshot.actions';
import { ScreenshotService } from '../services/screenshot.service';

@Injectable()
export class ScreenshotEffects {
  capture$ = createEffect(() =>
    this.actions$.pipe(
      ofType(ScreenshotActions.capture),
      switchMap(({ url, width }) =>
        this.screenshotService.capture({ url, width }).pipe(
          map((blobUrl) => ScreenshotActions.captureSuccess({ blobUrl })),
          catchError((error) =>
            of(ScreenshotActions.captureFailure({ error: error.message }))
          )
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private screenshotService: ScreenshotService
  ) {}
}

Angular Universal (SSR)

If you're using Angular Universal for server-side rendering, be careful: URL.createObjectURL() is not available in Node.js. Use platform detection:

import { Inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class ScreenshotService {
  private isBrowser: boolean;

  constructor(
    private http: HttpClient,
    @Inject(PLATFORM_ID) platformId: Object
  ) {
    this.isBrowser = isPlatformBrowser(platformId);
  }

  capture(options: ScreenshotOptions): Observable<string | null> {
    if (!this.isBrowser) {
      // SSR: return null, component handles the empty state
      return of(null);
    }

    // ... same blob URL logic as before
  }
}

In practice, screenshot captures are interactive actions that only happen in the browser, so it's often simpler to guard the entire capture call:

// In component:
capture(): void {
  if (!isPlatformBrowser(this.platformId)) return;
  // ... proceed
}

Standalone Component (Angular 15+)

For Angular 15+ with standalone components:

@Component({
  standalone: true,
  imports: [ReactiveFormsModule, NgIf, AsyncPipe],
  providers: [ScreenshotService],
  // ...
})
export class ScreenshotToolComponent { }

And provide HTTP client at the app level:

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideHttpClient(withInterceptors([screenshotAuthInterceptor])),
  ],
});

Get Your API Key

Get a free key at hermesforge.dev/screenshot — no credit card required. Full API docs at hermesforge.dev/api/docs.