NestJS has become the dominant framework for enterprise Node.js applications. Teams building AI backends, automated dashboards, and real-time services choose NestJS for its modular architecture, strong typing, and dependency injection.
But when you need to capture screenshots, generate PDFs, or create dynamic OG images, you hit a wall: Puppeteer requires 200MB+ of dependencies, doesn't work in serverless, and complicates your Docker builds.
One HTTP call fixes this. No browser binaries. Works everywhere — Lambda, Cloud Run, containers, and traditional VPS.
Why NestJS Developers Need This
Your NestJS app might need screenshots for:
- Automated reports — PDF exports of dashboards or data visualizations
- Dynamic OG images — Social cards generated from user content
- Compliance audits — Visual proof of data states, form submissions, or transactions
- AI agent workflows — Agents need to see web pages to make decisions
Without a hosted solution, your options all have serious trade-offs:
- Puppeteer: 200MB+ dependency bloat, Docker complexity, serverless incompatibility
- wkhtmltopdf: Legacy tool, CSS limitations, system-level dependencies
- Self-hosted Chrome: Infrastructure overhead, scaling headaches, memory management
Building a PageBolt Service for NestJS
The pattern: create an injectable PageboltService that wraps the API, then inject it wherever you need captures. This follows NestJS conventions — testable, swappable, and properly scoped.
Step 1: Install Dependencies
npm install @nestjs/axios axios
Step 2: Create the Injectable Service
Create src/services/pagebolt.service.ts:
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
@Injectable()
export class PageboltService {
private readonly baseUrl = 'https://pagebolt.dev/api/v1';
private readonly apiKey = process.env.PAGEBOLT_API_KEY;
constructor(private readonly httpService: HttpService) {}
async takeScreenshot(
url: string,
options?: {
width?: number;
height?: number;
format?: 'png' | 'jpeg' | 'webp';
fullPage?: boolean;
},
): Promise<Buffer> {
const response = await firstValueFrom(
this.httpService.post(
`${this.baseUrl}/screenshot`,
{
url,
width: options?.width ?? 1280,
height: options?.height ?? 720,
format: options?.format ?? 'png',
fullPage: options?.fullPage ?? false,
blockBanners: true,
blockAds: true,
},
{
headers: { 'x-api-key': this.apiKey },
responseType: 'arraybuffer',
},
),
);
return Buffer.from(response.data);
}
async generatePdf(
url: string,
options?: { landscape?: boolean },
): Promise<Buffer> {
const response = await firstValueFrom(
this.httpService.post(
`${this.baseUrl}/pdf`,
{
url,
landscape: options?.landscape ?? false,
printBackground: true,
},
{
headers: { 'x-api-key': this.apiKey },
responseType: 'arraybuffer',
},
),
);
return Buffer.from(response.data);
}
}
Step 3: Create the Controller
Create src/controllers/capture.controller.ts:
import { Controller, Post, Body, Res, HttpException, HttpStatus } from '@nestjs/common';
import { PageboltService } from '../services/pagebolt.service';
import { Response } from 'express';
@Controller('capture')
export class CaptureController {
constructor(private readonly pagebolt: PageboltService) {}
@Post('screenshot')
async screenshot(
@Body() body: { url: string },
@Res() res: Response,
) {
if (!body.url) {
throw new HttpException('url is required', HttpStatus.BAD_REQUEST);
}
const image = await this.pagebolt.takeScreenshot(body.url);
res.setHeader('Content-Type', 'image/png');
res.setHeader('Cache-Control', 'public, max-age=3600');
res.send(image);
}
@Post('pdf')
async pdf(
@Body() body: { url: string; landscape?: boolean },
@Res() res: Response,
) {
if (!body.url) {
throw new HttpException('url is required', HttpStatus.BAD_REQUEST);
}
const pdf = await this.pagebolt.generatePdf(body.url, {
landscape: body.landscape,
});
res.setHeader('Content-Type', 'application/pdf');
res.setHeader('Content-Disposition', 'attachment; filename="document.pdf"');
res.send(pdf);
}
}
Step 4: Register in Your Module
Update src/app.module.ts:
import { Module } from '@nestjs/common';
import { HttpModule } from '@nestjs/axios';
import { PageboltService } from './services/pagebolt.service';
import { CaptureController } from './controllers/capture.controller';
@Module({
imports: [HttpModule],
providers: [PageboltService],
controllers: [CaptureController],
exports: [PageboltService], // export if other modules need it
})
export class CaptureModule {}
Step 5: Set the Environment Variable
# .env
PAGEBOLT_API_KEY=your_key_here
Use @nestjs/config for production-grade config management, or load the key via your existing secrets manager.
Real-World Example: Compliance Audit Service
Inject PageboltService into any other service that needs captures:
import { Injectable } from '@nestjs/common';
import { PageboltService } from './pagebolt.service';
@Injectable()
export class AuditService {
constructor(private readonly pagebolt: PageboltService) {}
async captureFormSubmission(formUrl: string, userId: string): Promise<void> {
const screenshot = await this.pagebolt.takeScreenshot(formUrl);
await this.storeAuditProof(userId, screenshot);
}
async generateComplianceReport(dashboardUrl: string): Promise<Buffer> {
return this.pagebolt.generatePdf(dashboardUrl, { landscape: true });
}
private async storeAuditProof(userId: string, data: Buffer): Promise<void> {
// Store in S3, database, or your audit log
}
}
Testing the Service
Because PageboltService depends only on HttpService, it's straightforward to mock in unit tests:
import { Test } from '@nestjs/testing';
import { HttpService } from '@nestjs/axios';
import { PageboltService } from './pagebolt.service';
import { of } from 'rxjs';
describe('PageboltService', () => {
let service: PageboltService;
let httpService: jest.Mocked<HttpService>;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
PageboltService,
{
provide: HttpService,
useValue: { post: jest.fn() },
},
],
}).compile();
service = module.get(PageboltService);
httpService = module.get(HttpService);
});
it('calls the screenshot endpoint', async () => {
const fakeBuffer = Buffer.from('fake-png');
httpService.post.mockReturnValue(
of({ data: fakeBuffer.buffer } as any)
);
const result = await service.takeScreenshot('https://example.com');
expect(result).toBeInstanceOf(Buffer);
expect(httpService.post).toHaveBeenCalledWith(
expect.stringContaining('/screenshot'),
expect.objectContaining({ url: 'https://example.com' }),
expect.objectContaining({ headers: { 'x-api-key': undefined } }),
);
});
});
Comparison: PageBolt vs Self-Hosted Puppeteer
| Factor | PageBolt API | Puppeteer | wkhtmltopdf |
|---|---|---|---|
| Setup time | ~2 minutes | 30+ minutes | 20+ minutes |
| Docker image impact | None | +500MB | +200MB |
| Serverless support | Yes | No | No |
| Maintenance burden | Zero | High (crashes, memory leaks) | Legacy, unmaintained |
| CSS/JS rendering | Full Chromium | Full Chromium | Limited |
| Unit testability | Mock HTTP call | Requires browser process | Subprocess dependency |
Getting Started
- Get your API key — pagebolt.dev/dashboard (100 free requests/month, no credit card)
- Install deps —
npm install @nestjs/axios axios - Copy the service — Drop
pagebolt.service.tsinto your project - Add
PAGEBOLT_API_KEYto your environment - Inject and use —
POST /capture/screenshotwith a URL
Add web capture to your NestJS app today
Injectable service, TypeScript types, no browser dependencies. Works on Lambda, Cloud Run, and every container platform.
Get your free API key