Puppeteer PDF Implementation Plan (Hexagonal Architecture)
Overviewβ
Add Puppeteer to the NestJS backend using hexagonal architecture to generate professional PDFs from purchase order data, rendering them as business documents.
Architecture Overviewβ
Hexagonal Architecture Layersβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
β β PDF Service β β Controller β β Templates β β
β β (Puppeteer) β β (Endpoints) β β (HTML) β β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Application Layer β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
β β PDF Use Cases β β PDF Service β β PDF Ports β β
β β (Application β β (Orchestration) β β (Interfaces) β β
β β Services) β β β β β β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Domain Layer β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
β β PDF Entities β β PDF Value β β PDF Domain β β
β β (Models) β β Objects β β Services β β
β β β β β β β β
β βββββββββββββββββββ βββββββββββββββββββ ββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Phase 1: Domain Layer Setupβ
1.1 Create PDF Domain Structureβ
src/pdf/
βββ domain/
β βββ entities/
β β βββ pdf-document.entity.ts
β β βββ pdf-template.entity.ts
β βββ value-objects/
β β βββ pdf-options.vo.ts
β β βββ pdf-format.vo.ts
β β βββ pdf-quality.vo.ts
β βββ services/
β β βββ pdf-generation.service.ts
β βββ repositories/
β βββ pdf-repository.interface.ts
1.2 Define PDF Domain Entitiesβ
// PDF Document Entity
export class PdfDocument {
constructor(
public readonly id: string,
public readonly content: string,
public readonly format: PdfFormat,
public readonly options: PdfOptions,
public readonly createdAt: Date
) {}
}
// PDF Template Entity
export class PdfTemplate {
constructor(
public readonly id: string,
public readonly name: string,
public readonly htmlContent: string,
public readonly cssContent: string,
public readonly variables: string[]
) {}
}
1.3 Define Value Objectsβ
// PDF Options Value Object
export class PdfOptions {
constructor(
public readonly pageSize: string = "A4",
public readonly margins: {
top: number;
right: number;
bottom: number;
left: number;
},
public readonly printBackground: boolean = true,
public readonly timeout: number = 30000
) {}
}
// PDF Format Value Object
export class PdfFormat {
constructor(
public readonly width: string,
public readonly height: string,
public readonly unit: "mm" | "in" | "px" = "mm"
) {}
}
Phase 2: Application Layer Setupβ
2.1 Create Application Servicesβ
src/pdf/
βββ application/
β βββ use-cases/
β β βββ generate-purchase-order-pdf.use-case.ts
β β βββ generate-custom-pdf.use-case.ts
β β βββ preview-pdf.use-case.ts
β βββ services/
β β βββ pdf-orchestration.service.ts
β βββ ports/
β β βββ pdf-generator.port.ts
β β βββ template-renderer.port.ts
β β βββ pdf-storage.port.ts
β βββ dto/
β βββ generate-pdf.dto.ts
β βββ pdf-response.dto.ts
2.2 Define Application Ports (Interfaces)β
// PDF Generator Port
export interface PdfGeneratorPort {
generatePdf(html: string, options: PdfOptions): Promise<Buffer>;
generatePdfFromUrl(url: string, options: PdfOptions): Promise<Buffer>;
}
// Template Renderer Port
export interface TemplateRendererPort {
renderTemplate(
templateId: string,
data: Record<string, any>
): Promise<string>;
compileTemplate(html: string, css: string): Promise<string>;
}
// PDF Storage Port
export interface PdfStoragePort {
savePdf(pdfBuffer: Buffer, filename: string): Promise<string>;
getPdf(fileId: string): Promise<Buffer>;
deletePdf(fileId: string): Promise<void>;
}
2.3 Implement Use Casesβ
// Generate Purchase Order PDF Use Case
export class GeneratePurchaseOrderPdfUseCase {
constructor(
private readonly pdfGenerator: PdfGeneratorPort,
private readonly templateRenderer: TemplateRendererPort,
private readonly purchaseOrderRepository: PurchaseOrderRepositoryPort
) {}
async execute(
purchaseOrderId: string,
options?: PdfOptions
): Promise<Buffer> {
const purchaseOrder = await this.purchaseOrderRepository.findById(
purchaseOrderId
);
const html = await this.templateRenderer.renderTemplate(
"purchase-order",
purchaseOrder
);
return this.pdfGenerator.generatePdf(html, options || new PdfOptions());
}
}
Phase 3: Infrastructure Layer Setupβ
3.1 Create Infrastructure Adaptersβ
src/pdf/
βββ infrastructure/
β βββ adapters/
β β βββ puppeteer-pdf-generator.adapter.ts
β β βββ handlebars-template-renderer.adapter.ts
β β βββ file-system-pdf-storage.adapter.ts
β βββ controllers/
β β βββ pdf.controller.ts
β βββ repositories/
β β βββ pdf-file.repository.ts
β βββ templates/
β βββ purchase-order.template.html
β βββ purchase-order.template.css
β βββ template-config.json
3.2 Implement Puppeteer Adapterβ
// Puppeteer PDF Generator Adapter
@Injectable()
export class PuppeteerPdfGeneratorAdapter implements PdfGeneratorPort {
private browser: Browser | null = null;
async generatePdf(html: string, options: PdfOptions): Promise<Buffer> {
const browser = await this.getBrowser();
const page = await browser.newPage();
try {
await page.setContent(html, { waitUntil: "networkidle0" });
return await page.pdf({
format: options.pageSize,
margin: options.margins,
printBackground: options.printBackground,
timeout: options.timeout,
});
} finally {
await page.close();
}
}
private async getBrowser(): Promise<Browser> {
if (!this.browser) {
this.browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"],
});
}
return this.browser;
}
}
3.3 Implement Template Renderer Adapterβ
// Handlebars Template Renderer Adapter
@Injectable()
export class HandlebarsTemplateRendererAdapter implements TemplateRendererPort {
private handlebars = Handlebars.create();
private templates = new Map<string, HandlebarsTemplateDelegate>();
async renderTemplate(
templateId: string,
data: Record<string, any>
): Promise<string> {
const template = await this.getTemplate(templateId);
return template(data);
}
private async getTemplate(
templateId: string
): Promise<HandlebarsTemplateDelegate> {
if (!this.templates.has(templateId)) {
const templateContent = await this.loadTemplate(templateId);
const compiledTemplate = this.handlebars.compile(templateContent);
this.templates.set(templateId, compiledTemplate);
}
return this.templates.get(templateId)!;
}
}
Phase 4: Purchase Order Integrationβ
4.1 Extend Purchase Orders Module with Hexagonal Structureβ
src/purchase-orders/
βββ domain/
β βββ entities/
β β βββ purchase-order.entity.ts (extend with PDF methods)
β βββ services/
β βββ purchase-order-pdf.service.ts
βββ application/
β βββ use-cases/
β β βββ generate-purchase-order-pdf.use-case.ts
β βββ ports/
β βββ purchase-order-pdf.port.ts
βββ infrastructure/
βββ controllers/
βββ purchase-orders.controller.ts (add PDF endpoints)
4.2 Add PDF Ports to Purchase Order Domainβ
// Purchase Order PDF Port
export interface PurchaseOrderPdfPort {
generatePdf(purchaseOrderId: string, options?: PdfOptions): Promise<Buffer>;
generatePreview(purchaseOrderId: string): Promise<string>;
emailPdf(purchaseOrderId: string, email: string): Promise<void>;
}
4.3 Implement Purchase Order PDF Use Caseβ
@Injectable()
export class GeneratePurchaseOrderPdfUseCase {
constructor(
private readonly purchaseOrderRepository: PurchaseOrderRepositoryPort,
private readonly pdfGenerator: PdfGeneratorPort,
private readonly templateRenderer: TemplateRendererPort
) {}
async execute(
purchaseOrderId: string,
options?: PdfOptions
): Promise<Buffer> {
const purchaseOrder = await this.purchaseOrderRepository.findById(
purchaseOrderId
);
if (!purchaseOrder) {
throw new NotFoundException("Purchase order not found");
}
const templateData = this.mapPurchaseOrderToTemplateData(purchaseOrder);
const html = await this.templateRenderer.renderTemplate(
"purchase-order",
templateData
);
return this.pdfGenerator.generatePdf(html, options || new PdfOptions());
}
private mapPurchaseOrderToTemplateData(purchaseOrder: PurchaseOrder) {
return {
orderNumber: purchaseOrder.documentNumber,
supplier: purchaseOrder.supplier,
items: purchaseOrder.purchaseDetail.items,
totals: this.calculateTotals(purchaseOrder.purchaseDetail.items),
// ... other mappings
};
}
}
Phase 5: Module Configurationβ
5.1 Create PDF Moduleβ
@Module({
imports: [],
providers: [
// Domain Services
PdfGenerationService,
// Application Use Cases
GeneratePurchaseOrderPdfUseCase,
GenerateCustomPdfUseCase,
PreviewPdfUseCase,
// Infrastructure Adapters
{
provide: "PdfGeneratorPort",
useClass: PuppeteerPdfGeneratorAdapter,
},
{
provide: "TemplateRendererPort",
useClass: HandlebarsTemplateRendererAdapter,
},
{
provide: "PdfStoragePort",
useClass: FileSystemPdfStorageAdapter,
},
// Repositories
PdfFileRepository,
],
exports: [
GeneratePurchaseOrderPdfUseCase,
GenerateCustomPdfUseCase,
PreviewPdfUseCase,
],
})
export class PdfModule {}
5.2 Update Purchase Orders Moduleβ
@Module({
imports: [PdfModule],
providers: [
// Existing providers...
GeneratePurchaseOrderPdfUseCase,
],
controllers: [PurchaseOrdersController],
})
export class PurchaseOrdersModule {}
Phase 6: Controller Implementationβ
6.1 Add PDF Endpointsβ
@Controller("purchase-orders")
export class PurchaseOrdersController {
constructor(
private readonly generatePurchaseOrderPdfUseCase: GeneratePurchaseOrderPdfUseCase
) {}
@Get(":id/pdf")
@Header("Content-Type", "application/pdf")
@Header("Content-Disposition", 'attachment; filename="purchase-order.pdf"')
async generatePdf(
@Param("id") id: string,
@Query() options: PdfOptionsDto
): Promise<Buffer> {
return this.generatePurchaseOrderPdfUseCase.execute(id, options);
}
@Get(":id/print")
@Header("Content-Type", "text/html")
async generatePrintView(@Param("id") id: string): Promise<string> {
return this.generatePurchaseOrderPdfUseCase.generatePreview(id);
}
}
Phase 7: Frontend Integrationβ
7.1 Update Frontend Serviceβ
// purchaseOrderService.ts
export const generatePurchaseOrderPDF = async (
token: string,
purchaseOrderId: string,
options?: PdfOptions
): Promise<Blob> => {
const response = await fetch(
`${API_BASE_URL}/purchase-orders/${purchaseOrderId}/pdf`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
...(options && { body: JSON.stringify(options) }),
}
);
if (!response.ok) {
throw new Error("Failed to generate PDF");
}
return response.blob();
};
Implementation Order (Hexagonal Architecture)β
- Domain Layer - Entities, value objects, domain services
- Application Layer - Use cases, ports, DTOs
- Infrastructure Layer - Adapters, controllers, repositories
- Module Configuration - Dependency injection setup
- Integration - Connect with existing purchase order system
- Frontend Integration - Add PDF generation to UI
- Testing - Unit tests for each layer
- Deployment - Production configuration
Key Benefits of Hexagonal Architectureβ
- Separation of Concerns - Clear boundaries between layers
- Testability - Easy to mock dependencies and test use cases
- Flexibility - Can swap implementations (e.g., different PDF generators)
- Maintainability - Changes in one layer don't affect others
- Scalability - Easy to add new features and adapters
Testing Strategyβ
Domain Layer Testsβ
- Entity validation
- Value object constraints
- Domain service logic
Application Layer Testsβ
- Use case execution
- Business rule validation
- Port interface compliance
Infrastructure Layer Testsβ
- Adapter integration
- External service communication
- Error handling
Future Enhancementsβ
- Multiple PDF Generators - Puppeteer, jsPDF, PDFKit adapters
- Template Management - Dynamic template loading and caching
- Batch Processing - Queue-based PDF generation
- Caching Layer - Redis-based PDF caching
- Email Integration - PDF email service adapter