Saltar al contenido principal

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)​

  1. Domain Layer - Entities, value objects, domain services
  2. Application Layer - Use cases, ports, DTOs
  3. Infrastructure Layer - Adapters, controllers, repositories
  4. Module Configuration - Dependency injection setup
  5. Integration - Connect with existing purchase order system
  6. Frontend Integration - Add PDF generation to UI
  7. Testing - Unit tests for each layer
  8. 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