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| CProcess structure (cmd/pdf-server)
main.go builds a CLI app with two subcommands:
server— the long-running HTTP server. Its action calls the Wire injectorInitServer, then blocks onSIGINT/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 injectorBuildValidateService.
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/)
| Package | Responsibility |
|---|---|
app/server | Echo router, handlers, middleware (request-ID logging, panic recovery, OTel), static asset embedding, Swagger generation. |
app/services | Application services between handlers and the domain: PDFRenderService, ValidateService, StatsService, SpecService, and the domain error taxonomy. |
app/templates | Domain core: the Template/CompositeTemplate interfaces, the Store, the futures-based async render abstraction, and the validator. |
app/templates/fileloader | Loads templates from disk: engines, schema/params/assets/examples/partials loaders, plus composition and remote sub-packages. |
app/pdfrender | The PDFRenderer interface, the Config/Document types, and lifecycle-event definitions. |
app/pdfrender/cdp | The CDP implementation: browser pool, factory (runner/page), runner (process management), page, render. |
app/pdfrender/server | The loopback-only internal HTTP server the browser calls back into. |
app/config | GlobalConfig aggregating each package’s sub-config. |
app/log | logrus wrapper; context-carried loggers. |
app/utils/trace_utils | OpenTelemetry providers wired through middleware and spans. |
Key seams
- Rendering is asynchronous via a small
futurespackage. The HTML→PDF path runs on aPoolSchedulerwhose worker count equals the browser pool size. - The template store is hot-reloadable via a
StoreProvider/StoreConsumerLazyStore(see Template store & hot reload). - Errors are typed in
app/serviceswithIs/Unwrap; the handler’sbuildErroris the single place they map to HTTP status codes. - Tracing and logging are pervasive: components take a
TracerProviderand open spans; loggers are pulled from context withlog.LoggerFromContext.
Deeper dives
- Render pipeline — the end-to-end request flow.
- CDP browser pool — pooling, factory modes, process management.
- Internal callback server — how HTML reaches the browser and the callback event.
- Template store & hot reload.
- Extension points — add a provider, engine, helper, or loader.
- Codebase map — package-by-package with references.
The design/ directory contains PlantUML diagrams (main_flow.puml, services.puml, templates.puml).