Architecture

A map of the codebase: process structure, layered packages, and how a render request flows end to end.

PDF Server is a Go service that renders HTML templates to PDF by driving a headless browser over the Chrome DevTools Protocol (CDP). Module gitlab.com/c0va23/pdf-server. Web framework: Echo. CLI: urfave/cli v2. DI: Google Wire.

flowchart TD
  C[Client] -->|POST /render JSON| H[HTTP handler]
  H --> S[PDFRenderService]
  S --> V{schema valid?}
  V -- no --> E[422]
  V -- yes --> R[render body/header/footer]
  R --> I[(internal server<br/>stores HTML)]
  R --> P[browser pool]
  P -->|navigate| B[headless browser]
  B -->|GET HTML + assets| I
  B -->|printToPDF| P
  P -->|application/pdf| C

Process structure (cmd/pdf-server)

main.go builds a CLI app with two subcommands:

  • server — the long-running HTTP server. Its action calls the Wire injector InitServer, then blocks on SIGINT/SIGTERM.
  • validate — a one-shot that renders every example of every template and exits non-zero on failure (used in CI and while authoring). Calls the second injector BuildValidateService.

Dependency injection is wired with Google Wire. wire.go declares the two injectors; wire_bindings.go holds the single wireSet (all providers + interface bindings); wire_gen.go is generated. Reading InitServer in wire_gen.go top-to-bottom is the clearest single view of the object graph and its reverse-ordered cleanup chain.

Flags mutate the config structs in place (cmd/pdf-server/config.go) before Wire runs, so runtime configuration reaches the graph.

Layered packages (app/)

PackageResponsibility
app/serverEcho router, handlers, middleware (request-ID logging, panic recovery, OTel), static asset embedding, Swagger generation.
app/servicesApplication services between handlers and the domain: PDFRenderService, ValidateService, StatsService, SpecService, and the domain error taxonomy.
app/templatesDomain core: the Template/CompositeTemplate interfaces, the Store, the futures-based async render abstraction, and the validator.
app/templates/fileloaderLoads templates from disk: engines, schema/params/assets/examples/partials loaders, plus composition and remote sub-packages.
app/pdfrenderThe PDFRenderer interface, the Config/Document types, and lifecycle-event definitions.
app/pdfrender/cdpThe CDP implementation: browser pool, factory (runner/page), runner (process management), page, render.
app/pdfrender/serverThe loopback-only internal HTTP server the browser calls back into.
app/configGlobalConfig aggregating each package’s sub-config.
app/loglogrus wrapper; context-carried loggers.
app/utils/trace_utilsOpenTelemetry providers wired through middleware and spans.

Key seams

  • Rendering is asynchronous via a small futures package. The HTML→PDF path runs on a PoolScheduler whose worker count equals the browser pool size.
  • The template store is hot-reloadable via a StoreProvider/StoreConsumer LazyStore (see Template store & hot reload).
  • Errors are typed in app/services with Is/Unwrap; the handler’s buildError is the single place they map to HTTP status codes.
  • Tracing and logging are pervasive: components take a TracerProvider and open spans; loggers are pulled from context with log.LoggerFromContext.

Deeper dives

The design/ directory contains PlantUML diagrams (main_flow.puml, services.puml, templates.puml).