Advanced
Architecture
Understand the 4-stage rendering pipeline, package structure, and design principles behind NextReport Engine.
Overview
NextReport Engine is a schema-driven reporting engine built on a strict separation of concerns. The core insight: report generation is a data transformation pipeline, not a monolithic render call.
The 4-stage pipeline
Every report passes through four sequential stages:
Schema + Data
│
▼
┌─────────────┐
│ 1. Validate │ Verify schema structure and types
└──────┬──────┘
│
▼
┌─────────────┐
│ 2. Expression│ Resolve {{...}} expressions against data
└──────┬──────┘
│
▼
┌─────────────┐
│ 3. Band │ Expand detail bands, apply data binding
└──────┬──────┘
│
▼
┌─────────────┐
│ 4. Layout │ Position elements, calculate page breaks
└──────┬──────┘
│
▼
RenderResult (IR)
│
▼
HTML / PDF / Custom
Stage 1: Validate
The validator checks the schema for structural correctness: required fields, valid types, known enums, balanced expression delimiters, and component bounds. Validation errors halt the pipeline immediately.
const validation = validateReport(schema)
if (!validation.valid) {
// Stop here -- schema is broken
}
Stage 2: Expression
The expression engine parses {{...}} delimiters and evaluates them against the data context. Built-in and custom functions are resolved. The output is a schema with all expressions replaced by concrete values.
Stage 3: Band
The band engine processes each band in order:
- Header/Footer bands render once with the top-level data context
- Detail bands iterate over the bound array, producing one rendered band per element
- The iterator variable is scoped to each iteration
Stage 4: Layout
The layout engine takes the expanded bands and positions them on pages. It calculates page breaks based on band heights and page dimensions, producing the final RenderResult with positioned elements.
Intermediate Representation (IR)
The RenderResult is the engine’s intermediate representation — a format-agnostic description of the rendered report:
interface RenderResult {
pages: Page[]
metadata: {
pageCount: number
renderTimeMs: number
}
}
interface Page {
width: number
height: number
elements: ResolvedElement[]
}
interface ResolvedElement {
type: 'text' | 'table'
position: { x: number; y: number; width: number; height: number }
content: string
style: {
fontSize?: number
fontWeight?: string
fontFamily?: string
color?: string
textAlign?: string
}
}
The IR decouples the rendering logic from output format concerns. Built-in renderers (renderToHtml, renderToPdf) consume the IR, and you can write custom renderers for any format.
Package structure
The engine is organized as a monorepo with strict module boundaries:
nextreport/engine
├── packages/
│ ├── engine-core/ # Schema types, validation, shared contracts
│ ├── engine-expression/ # Expression parser and evaluator
│ ├── engine-band/ # Band expansion and data binding
│ ├── engine-layout/ # Page layout and pagination
│ ├── engine-render/ # HTML and PDF renderers
│ └── engine/ # Public API facade
├── apps/
│ └── server/ # REST API server
└── docs/ # Documentation
Dependency graph
engine (public API)
├── engine-core
├── engine-expression → engine-core
├── engine-band → engine-core, engine-expression
├── engine-layout → engine-core
└── engine-render → engine-core
Each package depends only on engine-core for shared types, plus its direct upstream stage. There are no circular dependencies.
Engine isolation rule
The most important architectural constraint:
engine-core must never become a god-package.
Each package owns its domain logic. Shared types live in engine-core, but business logic stays in the package that owns the stage. For example:
- Expression parsing logic lives in
engine-expression, notengine-core - Band iteration logic lives in
engine-band, notengine-core - Layout calculations live in
engine-layout, notengine-core
This ensures packages can evolve independently and be tested in isolation.
Design principles
Schema-first
The report schema is the source of truth. It is a plain JSON/TypeScript object that fully describes the report without requiring code. This enables:
- Schema validation before rendering
- Schema storage and versioning
- Visual designer tools that edit the schema directly
- Cross-language compatibility (any language that speaks JSON can define schemas)
Deterministic rendering
Given the same schema and data, the engine always produces the same output. There is no hidden state, no random behavior, and no dependency on system time (unless the data includes it).
Progressive complexity
Simple reports require minimal schema. Advanced features (custom functions, complex data binding, multi-page layouts) are opt-in. A valid report can be as small as:
{
version: '1.0',
type: 'report',
page: { size: 'A4', orientation: 'portrait' },
bands: [],
}
Format independence
The IR (intermediate representation) is the contract between the pipeline and renderers. Adding a new output format (Excel, Markdown, SVG) requires only a new renderer function — the pipeline does not change.
Next steps
- Custom Renderer — build renderers that consume the IR
- Custom Functions — extend the expression engine
- Schema Overview — the schema structure reference