Puppeteer provider for NestJS with a high-level API inspired by the Cloudflare Browser Rendering REST API.
- Service layer --
PuppeteerServicewith 7 Cloudflare-aligned methods - REST layer -- Auto-generated endpoints with configurable prefix, guards, and feature selection
- Feature modules -- Standalone modules for individual features (
PdfBrowserModule,ScreenshotBrowserModule, etc.) - DI decorators --
@InjectBrowser(),@InjectContext(),@InjectPage()for direct Puppeteer access - Validation -- DTOs with class-validator, registered via
APP_PIPE - Swagger -- OpenAPI decorators on all endpoints and DTOs
- Error handling -- Cloudflare-format error responses via
APP_FILTER - Testing --
createMockPuppeteerProviders()for unit tests
npm install @bitwild/nest-puppeteer puppeteerFor REST endpoints (optional):
npm install class-validator class-transformer @nestjs/swaggerimport { Module } from '@nestjs/common';
import { PuppeteerModule } from '@bitwild/nest-puppeteer';
@Module({
imports: [PuppeteerModule.forRoot()],
})
export class AppModule {}Then inject the service:
import { Injectable } from '@nestjs/common';
import { PuppeteerService } from '@bitwild/nest-puppeteer';
@Injectable()
export class ReportService {
constructor(private readonly puppeteer: PuppeteerService) {}
async generateInvoice(url: string): Promise<Buffer> {
return this.puppeteer.pdf({
url,
format: 'a4',
printBackground: true,
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
});
}
async getPageContent(url: string): Promise<string> {
return this.puppeteer.content({
url,
waitForSelector: { selector: '#main-content', visible: true },
});
}
}Expose Cloudflare-compatible HTTP endpoints:
import { Module } from '@nestjs/common';
import { PuppeteerModule } from '@bitwild/nest-puppeteer';
import { AuthGuard } from './auth.guard';
@Module({
imports: [
PuppeteerModule.forRoot({
headless: true,
rest: {
prefix: 'browser-rendering',
features: ['content', 'screenshot', 'pdf', 'markdown', 'scrape', 'links'],
guards: [AuthGuard],
},
}),
],
})
export class AppModule {}This registers:
| Method | Path | Description |
|---|---|---|
| POST | /browser-rendering/content |
Fetch rendered HTML |
| POST | /browser-rendering/screenshot |
Capture screenshot (binary) |
| POST | /browser-rendering/pdf |
Generate PDF (binary) |
| POST | /browser-rendering/markdown |
Extract Markdown |
| POST | /browser-rendering/snapshot |
HTML + screenshot in one call |
| POST | /browser-rendering/scrape |
Scrape elements by CSS selectors |
| POST | /browser-rendering/links |
Extract all links |
Request example:
curl -X POST http://localhost:3000/browser-rendering/pdf \
-H 'Content-Type: application/json' \
-d '{"url": "https://example.com", "format": "a4", "printBackground": true}' \
--output document.pdfJSON response format (content, markdown, scrape, links):
{
"success": true,
"result": { "html": "..." }
}Error response format:
{
"success": false,
"errors": [{ "code": 400, "message": "Either \"url\" or \"html\" must be provided" }]
}import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PuppeteerModule } from '@bitwild/nest-puppeteer';
@Module({
imports: [
PuppeteerModule.forRootAsync({
imports: [ConfigModule],
useFactory: (config: ConfigService) => ({
launchOptions: {
headless: config.get('PUPPETEER_HEADLESS', true),
args: config.get('PUPPETEER_ARGS', '').split(',').filter(Boolean),
},
}),
inject: [ConfigService],
rest: {
prefix: 'api/browser',
features: ['pdf', 'screenshot'],
guards: [AuthGuard],
},
}),
],
})
export class AppModule {}Pre-configured modules for individual features with their own defaults, REST endpoint, and guards.
import { Module } from '@nestjs/common';
import { PdfBrowserModule } from '@bitwild/nest-puppeteer';
@Module({
imports: [
PdfBrowserModule.forRoot({
launchOptions: { headless: true },
defaults: {
format: 'a4',
printBackground: true,
margin: { top: '1cm', bottom: '1cm', left: '1cm', right: '1cm' },
},
prefix: 'api/pdf', // POST /api/pdf
guards: [AuthGuard],
}),
],
})
export class AppModule {}import { Module } from '@nestjs/common';
import {
PuppeteerModule,
PdfBrowserModule,
ScreenshotBrowserModule,
ScrapeBrowserModule,
} from '@bitwild/nest-puppeteer';
@Module({
imports: [
// Shared browser
PuppeteerModule.forRoot({ headless: true }),
// Feature modules with individual config
PdfBrowserModule.register({
defaults: { format: 'a4', printBackground: true },
prefix: 'api/pdf',
guards: [AuthGuard],
}),
ScreenshotBrowserModule.register({
defaults: { fullPage: true, type: 'png' },
prefix: 'api/screenshot',
guards: [AuthGuard],
}),
ScrapeBrowserModule.register({
prefix: 'api/scrape',
guards: [AuthGuard],
}),
],
})
export class AppModule {}Each feature module provides a dedicated service:
| Module | Service | Method |
|---|---|---|
PdfBrowserModule |
PdfBrowserService |
.generate(options) |
ScreenshotBrowserModule |
ScreenshotBrowserService |
.capture(options) |
ContentBrowserModule |
ContentBrowserService |
.fetch(options) |
MarkdownBrowserModule |
MarkdownBrowserService |
.extract(options) |
SnapshotBrowserModule |
SnapshotBrowserService |
.take(options) |
ScrapeBrowserModule |
ScrapeBrowserService |
.scrape(options) |
LinksBrowserModule |
LinksBrowserService |
.extract(options) |
Feature services merge module defaults with per-call options:
@Injectable()
export class InvoiceService {
constructor(private readonly pdf: PdfBrowserService) {}
async generate(url: string): Promise<Buffer> {
// Module defaults (format: a4, margins, etc.) applied automatically
return this.pdf.generate({ url });
}
async generateLandscape(url: string): Promise<Buffer> {
// Override specific defaults per call
return this.pdf.generate({ url, landscape: true });
}
}Inject Puppeteer primitives directly:
import { Injectable } from '@nestjs/common';
import { InjectBrowser, InjectPage } from '@bitwild/nest-puppeteer';
import { Browser, Page } from 'puppeteer';
@Injectable()
export class CustomService {
constructor(
@InjectBrowser() private readonly browser: Browser,
@InjectPage() private readonly page: Page,
) {}
async doCustomWork() {
const page = await this.browser.newPage();
try {
await page.goto('https://example.com');
// ... custom puppeteer logic
} finally {
await page.close();
}
}
}@Module({
imports: [PuppeteerModule.forFeature(['crawler', 'renderer'])],
})
export class CrawlerModule {}
// Then inject:
@Injectable()
export class CrawlerService {
constructor(
@InjectPage('crawler') private readonly crawlerPage: Page,
@InjectPage('renderer') private readonly rendererPage: Page,
) {}
}@Module({
imports: [
PuppeteerModule.forRoot({ headless: true }, 'chrome'),
PuppeteerModule.forRoot({ headless: true }, 'stealth'),
],
})
export class AppModule {}
// Inject specific instances:
@Injectable()
export class MyService {
constructor(
@InjectBrowser('chrome') private readonly chrome: Browser,
@InjectBrowser('stealth') private readonly stealth: Browser,
) {}
}All methods accept a common set of options plus method-specific fields. Options are flat (not nested) to match the Cloudflare API.
interface CommonBrowserOptions {
url?: string; // URL to navigate to
html?: string; // HTML to render directly
authenticate?: { username, password }; // HTTP Basic Auth
cookies?: CookieParam[]; // Cookies to set
gotoOptions?: { waitUntil, timeout }; // Navigation behavior
setExtraHTTPHeaders?: Record<string, string>;
rejectResourceTypes?: ResourceType[]; // Block resource types
rejectRequestPattern?: string[]; // Block URL patterns (regex)
allowResourceTypes?: ResourceType[]; // Allow only these types
allowRequestPattern?: string[]; // Allow only these patterns
userAgent?: string;
waitForSelector?: { selector, timeout?, visible? };
waitForTimeout?: number; // Static delay (ms)
viewport?: { width, height, deviceScaleFactor };
addScriptTag?: { url?, content? }[];
addStyleTag?: { url?, content? }[];
setJavaScriptEnabled?: boolean;
emulateMediaType?: string; // 'screen' | 'print'
}const html = await puppeteerService.content({
url: 'https://example.com',
waitForSelector: { selector: '#app', visible: true },
});const buffer = await puppeteerService.screenshot({
url: 'https://example.com',
fullPage: true,
type: 'png',
quality: 90, // jpeg/webp only
omitBackground: true,
selector: '#chart', // screenshot a specific element
});const buffer = await puppeteerService.pdf({
url: 'https://example.com',
format: 'a4',
landscape: true,
printBackground: true,
scale: 0.8,
margin: { top: '2cm', bottom: '2cm', left: '1cm', right: '1cm' },
displayHeaderFooter: true,
headerTemplate: '<div style="font-size:10px">Header</div>',
footerTemplate: '<div style="font-size:10px">Page <span class="pageNumber"></span></div>',
});const md = await puppeteerService.markdown({
url: 'https://example.com/article',
rejectResourceTypes: ['image', 'stylesheet'],
});const { html, screenshot } = await puppeteerService.snapshot({
url: 'https://example.com',
fullPage: true,
type: 'jpeg',
quality: 80,
});const results = await puppeteerService.scrape({
url: 'https://example.com',
selectors: ['h1', 'p.intro', 'a[href]'],
});
// results: [{ selector: 'h1', elements: [{ text, html, attributes, width, height, top, left }] }, ...]const urls = await puppeteerService.links({
url: 'https://example.com',
visibleLinksOnly: true,
});
// urls: ['https://example.com/about', 'https://example.com/contact', ...]If @nestjs/swagger is installed, all endpoints and DTOs are auto-documented:
import { NestFactory } from '@nestjs/core';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
const app = await NestFactory.create(AppModule);
const config = new DocumentBuilder()
.setTitle('Browser Rendering API')
.setVersion('1.0')
.build();
SwaggerModule.setup('docs', app, SwaggerModule.createDocument(app, config));
await app.listen(3000);Use createMockPuppeteerProviders() to avoid launching a real browser:
import { Test } from '@nestjs/testing';
import { createMockPuppeteerProviders, PuppeteerService } from '@bitwild/nest-puppeteer';
describe('ReportService', () => {
let service: ReportService;
beforeEach(async () => {
const module = await Test.createTestingModule({
providers: [
ReportService,
PuppeteerService,
...createMockPuppeteerProviders({
browser: {
newPage: jest.fn().mockResolvedValue({
goto: jest.fn(),
pdf: jest.fn().mockResolvedValue(Buffer.from('pdf')),
content: jest.fn().mockResolvedValue('<html></html>'),
close: jest.fn(),
setViewport: jest.fn(),
}),
},
}),
],
}).compile();
service = module.get(ReportService);
});
});Recommended launch options for containerized environments:
PuppeteerModule.forRoot({
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
],
})MIT