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