Taking Screenshots in Angular with a Screenshot API
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.