A modern, schema-driven document rendering platform.
Design visually. Render via API. Integrate anywhere.
Why? • Quick Start • Architecture • Schema DSL • API • Contributing • Roadmap
Visual Designer with component toolbox, band manager, data fields, property editor, and live preview
NextReport Engine is an open-source, headless reporting engine with a visual designer and runtime viewer. It replaces legacy reporting tools (JasperReports, FastReport, Crystal Reports) with a modern, web-native stack.
Who is this for?
- Frontend developers — Embed the viewer or designer in React/Next.js apps with zero config
- Backend developers — Generate PDF/HTML reports via a stateless REST API from Java, .NET, Python, or any HTTP client
- SaaS builders — Add reporting to your product without building a rendering engine from scratch
- Business users — Design reports visually with drag & drop, no code required
- AI/Agent builders — Generate reports programmatically with a JSON schema that LLMs can produce natively
- Teams migrating from legacy tools — Replace JasperReports, FastReport, or Crystal Reports with a modern, maintainable stack
Reporting in 2026 is stuck between two bad options:
Enterprise tools are powerful but painful. JasperReports has a steep learning curve, relies on XML-based schemas (JRXML), and its desktop designer feels like software from a different era. Crystal Reports is similar — capable but heavy. The development experience is far from what modern teams expect.
Modern alternatives are closed or expensive. FastReport and Stimulsoft offer better UX, but they're proprietary and come with per-developer or per-server licensing. ActiveReportsJS is enterprise-grade but locked to the GrapeCity ecosystem. If your budget is limited or you need to customize deeply, these are dead ends.
The open-source gap is real. On the JavaScript/TypeScript side, there is no mature, full-featured reporting engine. Plenty of projects attempt a piece of the puzzle — a PDF library here, a drag-and-drop builder there — but a report is actually five systems working together: a layout engine, a visual designer, an expression engine, a data binding layer, and a multi-format renderer. Projects that tackle only one or two of these fail to deliver a usable product.
NextReport Engine is built on the insight that all five systems must be designed as one coherent architecture, but with strict boundaries so they can evolve independently.
- Schema-driven core — The JSON DSL is the contract between all systems. The engine doesn't know about the designer. The renderer doesn't know about the canvas. They all speak schema.
- Developer-first, then visual — The API and schema work standalone. The designer is a visual layer on top, not a requirement. You can generate reports from code, from an API call, or from an AI agent — the designer is optional.
- Built for the modern web stack — TypeScript, React, Next.js, Tailwind. Not a Java applet. Not a desktop app. Not a legacy ActiveX control. Native web, from the ground up.
- Open source to the core — The engine, designer, viewer, renderers, expression engine — all open. No "community edition" that's missing the features you actually need.
| NextReport | JasperReports | FastReport | Stimulsoft | ActiveReportsJS | jsreport | Syncfusion | |
|---|---|---|---|---|---|---|---|
| Open Source | Full | Partial | No | No | No | Partial | No |
| Web-Native | Yes | No (Java) | Partial | Yes | Yes | Yes (Node.js) | Partial (.NET backend) |
| Visual Designer | WYSIWYG | Desktop only | Desktop + Web | Web | Web | Code-based | Web (RDL) |
| Modern UX | Yes | No | Moderate | Yes | Moderate | Developer-oriented | Enterprise |
| API-First | Yes | No | No | Partial | Partial | Yes | Needs .NET server |
| React Integration | Native | None | None | Wrapper | Wrapper | None (standalone) | Wrapper (.NET dep) |
| Report Schema | JSON | XML (JRXML) | Proprietary | Proprietary | Proprietary | HTML/Handlebars | XML (RDL/RDLC) |
| AI/LLM-Ready | Yes (JSON DSL) | No | No | No | No | No | No |
| Pricing | Free | Free / Paid | Paid | Paid | Paid | Free (5 templates) / Paid | Free (small teams) / Paid |
| Learning Curve | Low | High | Medium | Medium | Medium | Medium | Medium-High |
| Self-Hosted | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
Based on the market gap, NextReport is most valuable for:
- SaaS products that need embedded reporting without licensing costs
- Internal tool teams building admin dashboards, invoice systems, or data exports
- Startups that can't justify enterprise reporting licenses but need more than an HTML-to-PDF hack
- Java/.NET teams migrating away from JasperReports who want a modern web-based alternative with a clean REST API
- AI/automation builders who need a reporting DSL that language models can generate natively
- Visual Report Designer — Three-panel layout with component toolbox, canvas area, and property editor. Drag & drop from toolbox, resize handles, keyboard shortcuts (Ctrl+Z, Delete, Escape), undo/redo, zoom, live preview.
- 4 Component Types — Text, Table, Image (URL/base64), and Barcode/QR Code (Code128, QR). All expression-bound.
- 7 Band Types — Header, Detail, Footer, Group Header/Footer (with
groupByand subtotals), Page Header/Footer (with{{pageNumber}}/{{totalPages}}). - Runtime Viewer — Render reports in the browser with pagination, print, and PDF export.
- Headless API — Stateless REST endpoints for rendering, validation, preview, and template CRUD. Send JSON, get HTML or PDF.
- Template Management — Full CRUD API for templates with metadata (name, description, version, timestamps). Save/load from designer.
- Schema-Driven DSL — Reports are defined as JSON. Human-readable, version-controllable, LLM-friendly.
- Expression Engine — Template expressions with 9 built-in functions:
formatCurrency,formatDate,formatNumber,sum,count,uppercase,lowercase,concat,if. - Pluggable Renderers — HTML and PDF out of the box. Barcode/QR rendered as inline SVG via bwip-js. Add new output formats without touching the core.
- Canvas Abstraction — Port/adapter pattern isolates the canvas library. Swap rendering backends without changing application code.
- Type-Safe — Written in TypeScript with Zod schemas. Full type inference from schema to render output.
- Locale-Aware — Currency, date, and number formatting respects report locale.
formatCurrency(price, 'TRY')withlocale: "tr-TR"outputs₺45.000,00.
npm install @nextreport/enginepnpm add @nextreport/engineyarn add @nextreport/engineRequirements: Node.js >= 22. For PDF generation, Puppeteer is an optional dependency — install it separately if needed: npm install puppeteer
import { renderReport, renderToHtml, validateReport } from '@nextreport/engine'
// Define your report schema
const schema = {
type: 'report' as const,
locale: 'en-US',
dataSource: { type: 'json' as const },
bands: [
{
type: 'header' as const,
height: 60,
components: [
{
type: 'text' as const,
position: { x: 0, y: 10, width: 300, height: 30 },
content: '{{companyName}}',
fontSize: 24,
fontWeight: 'bold' as const,
},
],
},
{
type: 'detail' as const,
height: 25,
dataBinding: 'items',
components: [
{
type: 'text' as const,
position: { x: 0, y: 0, width: 200, height: 25 },
content: '{{item.name}}',
},
{
type: 'text' as const,
position: { x: 300, y: 0, width: 100, height: 25 },
content: '{{formatCurrency(item.price, "USD")}}',
},
],
},
{
type: 'footer' as const,
height: 30,
components: [
{
type: 'text' as const,
position: { x: 300, y: 0, width: 100, height: 25 },
content: 'Total: {{formatCurrency(sum(items, "price"), "USD")}}',
fontWeight: 'bold' as const,
},
],
},
],
}
// Your data
const data = {
companyName: 'Acme Corp',
items: [
{ name: 'Widget', price: 29.99 },
{ name: 'Gadget', price: 49.99 },
],
}
// Validate
const validation = validateReport(schema)
console.log(validation.valid) // true
// Render to HTML
const ir = renderReport(schema, data)
const html = renderToHtml(ir)
// Render to PDF (requires puppeteer)
import { renderToPdf } from '@nextreport/engine'
const pdf = await renderToPdf(html)To run the complete platform with visual designer, viewer, and API server:
- Node.js >= 22
- pnpm 10.33+
git clone https://github.com/nextreport/engine.git
cd nextreport-engine
pnpm install
pnpm run devOpen http://localhost:3000 in your browser.
- Create a report: http://localhost:3000/new
- Designer with invoice template: http://localhost:3000/designer?template=invoice-v1
- View rendered invoice: http://localhost:3000/viewer?template=invoice-v1
pnpm test # 428 tests across all packages
pnpm run lint # ESLint with engine isolation rules
pnpm run build # Production buildcurl -X POST http://localhost:3000/api/render \
-H 'Content-Type: application/json' \
-d '{
"templateId": "invoice-v1",
"data": {
"companyName": "Acme Corp",
"invoiceNumber": "INV-001",
"invoiceDate": "2026-04-12",
"items": [
{ "name": "Laptop", "quantity": 1, "unitPrice": 45000, "total": 45000 },
{ "name": "Mouse", "quantity": 3, "unitPrice": 750, "total": 2250 }
]
}
}'Returns HTML by default. Add "format": "pdf" for PDF output.
React/Next.js — Viewer Component:
import { ReportViewer } from '@nextreport/ui-viewer'
;<ReportViewer report={reportSchema} data={reportData} />Any Language — REST API:
# Python
response = requests.post("http://localhost:3000/api/render", json={
"templateId": "invoice-v1",
"data": invoice_data,
"format": "pdf"
})
pdf_bytes = response.content// Java
HttpResponse response = client.post("/api/render", Map.of(
"templateId", "invoice-v1",
"data", invoiceData,
"format", "pdf"
));
byte[] pdf = response.body();// C#
var response = await client.PostAsJsonAsync("/api/render", new {
templateId = "invoice-v1",
data = invoiceData,
format = "pdf"
});
var pdf = await response.Content.ReadAsByteArrayAsync();External consumers send simple, flat JSON data — they never need to know about the report schema DSL.
┌─────────────────────────────────────────────────────┐
│ apps/web (Next.js) │
│ Designer UI │ Viewer UI │ API Routes │
└────────┬────────┴──────┬──────┴──────┬──────────────┘
│ │ │
┌────┴────┐ ┌──────┴──────┐ ┌───┴────────────┐
│ ui- │ │ ui-viewer │ │ renderer-html │
│designer │ │ │ │ renderer-pdf │
└────┬────┘ └──────┬──────┘ └───┬────────────┘
│ │ │
┌────┴────┐ │ ┌────┴────┐
│ canvas │ │ │ engine- │
│ (ports +│ └────────┤ core │
│ konva) │ │ │
└────┬────┘ └────┬────┘
│ │
└──────────┬──────────────────┘
│
┌─────┴─────┐
│ schema │
│ (Zod) │
└───────────┘
- Schema is the source of truth — Everything flows from JSON. Schema defines the report, engine processes it, renderers output it.
- Engine isolation —
engine-coreand renderers are pure TypeScript. No React, no Next.js, no Konva. They can run anywhere. - Strict dependency boundaries — Each package has explicit, enforced dependencies. No circular imports. Internal modules are not exposed.
- Pluggable everything — Renderers, components, canvas backends — all swappable through interfaces.
- Stateless API — The engine has no database, no sessions, no side effects. Send schema + data, get output.
| Package | Description | Dependencies |
|---|---|---|
@nextreport/schema |
Zod schemas, TypeScript types, JSON Schema export | None |
@nextreport/engine-core |
Render pipeline: validate → expression → band → layout → IR | schema |
@nextreport/renderer-html |
IR → self-contained HTML with inline styles | schema, engine-core |
@nextreport/renderer-pdf |
HTML → PDF via Puppeteer | schema, engine-core, renderer-html |
@nextreport/canvas |
Canvas abstraction: port interfaces + Konva adapter | schema |
@nextreport/ui-designer |
Visual designer React components + Zustand store | schema, engine-core, canvas |
@nextreport/ui-viewer |
Report viewer React component | schema, engine-core, renderer-html |
schema → depends on nothing (root of the graph)
engine-core → schema only
renderer-* → schema + engine-core
canvas → schema only (types)
ui-designer → canvas + schema + engine-core
ui-viewer → renderer-html + schema + engine-core
apps/web → all packages
Forbidden: engine-core never imports React, Next.js, or Konva. Renderers never import UI packages. No package imports from apps/web.
Reports are defined as JSON:
{
"version": "1.0",
"type": "report",
"locale": "tr-TR",
"page": {
"size": "A4",
"orientation": "portrait",
"margins": { "top": 20, "right": 15, "bottom": 20, "left": 15 }
},
"dataSource": { "type": "json" },
"bands": [
{
"type": "header",
"height": 80,
"components": [
{
"type": "text",
"position": { "x": 0, "y": 10, "width": 300, "height": 30 },
"content": "{{companyName}}",
"fontSize": 24,
"fontWeight": "bold"
}
]
},
{
"type": "detail",
"height": 25,
"dataBinding": "items",
"components": [
{
"type": "text",
"position": { "x": 0, "y": 0, "width": 200, "height": 25 },
"content": "{{item.name}}"
},
{
"type": "text",
"position": { "x": 400, "y": 0, "width": 100, "height": 25 },
"content": "{{formatCurrency(item.price, 'TRY')}}"
}
]
},
{
"type": "footer",
"height": 40,
"components": [
{
"type": "text",
"position": { "x": 400, "y": 5, "width": 135, "height": 30 },
"content": "Total: {{formatCurrency(sum(items, 'price'), 'TRY')}}",
"fontWeight": "bold"
}
]
}
]
}| Type | Behavior | Status |
|---|---|---|
header |
Rendered once at the top | v0.1 |
detail |
Repeated for each item in dataBinding array |
v0.1 |
footer |
Rendered once at the bottom | v0.1 |
group-header |
Before each group (groupBy field, _groupKey context) |
v0.2 |
group-footer |
After each group (subtotals via sum(_groupItems, 'field')) |
v0.2 |
page-header |
Top of every page | v0.2 |
page-footer |
Bottom of every page ({{pageNumber}} / {{totalPages}}) |
v0.2 |
| Type | Properties | Status |
|---|---|---|
text |
content, fontSize, fontWeight, color, textAlign | v0.1 |
table |
dataBinding, columns (header, field, width) | v0.1 |
image |
src (URL/base64/expression), alt, objectFit | v0.2 |
barcode |
value (expression), format (qr/code128), color | v0.2 |
Expressions use {{...}} delimiters with function call syntax:
{{variableName}} → Simple variable
{{customer.name}} → Nested access
{{formatCurrency(item.price, 'TRY')}} → Function call
{{formatDate(orderDate, 'DD.MM.YYYY')}} → Date formatting
{{sum(items, 'total')}} → Aggregation
{{if(total > 1000, 'VIP', 'Standard')}} → Conditional
Built-in Functions:
| Function | Example | Description |
|---|---|---|
formatCurrency(value, currency) |
formatCurrency(price, 'TRY') |
Locale-aware currency |
formatDate(value, pattern) |
formatDate(date, 'DD.MM.YYYY') |
Date formatting |
formatNumber(value, decimals) |
formatNumber(rate, 2) |
Number formatting |
sum(array, field) |
sum(items, 'price') |
Array field sum |
count(array) |
count(items) |
Array length |
uppercase(value) |
uppercase(name) |
To upper case |
lowercase(value) |
lowercase(name) |
To lower case |
concat(a, b, ...) |
concat(first, ' ', last) |
String join |
if(condition, then, else) |
if(qty > 0, 'In Stock', 'Out') |
Conditional |
Detail bands iterate over arrays. The iterator variable name is derived automatically by removing the trailing s:
dataBinding: "items"→ iterator:item→ access:{{item.name}}dataBinding: "employees"→ iterator:employee→ access:{{employee.email}}
Custom iterator name via iteratorName field on the band.
The expression engine handles type mismatches gracefully. Java or .NET clients often send numbers as strings — the engine coerces automatically:
{ "price": "45000" }formatCurrency(price, 'TRY') → ₺45.000,00 (string "45000" coerced to number)
Render a report to HTML or PDF.
Request:
{
"templateId": "invoice-v1",
"data": { "companyName": "Acme", "items": [...] },
"format": "html"
}Or with inline template:
{
"template": { "type": "report", ... },
"data": { ... },
"format": "pdf"
}Response (HTML):
{
"success": true,
"output": "<!DOCTYPE html>...",
"metadata": { "totalPages": 1, "generatedAt": "2026-04-12T...", "locale": "tr-TR" }
}Response (PDF): Binary PDF stream with Content-Type: application/pdf.
Validate a report schema without rendering.
// Request
{ "template": { "type": "report", ... } }
// Response (valid)
{ "valid": true, "errors": [], "warnings": [] }
// Response (invalid)
{ "valid": false, "errors": [{ "path": "dataSource", "message": "Required" }], "warnings": [] }Lightweight render — first page only, always HTML.
// Request
{ "templateId": "invoice-v1", "data": { ... } }
// Response
{ "success": true, "output": "<!DOCTYPE html>...", "metadata": { "totalPages": 1 } }List all templates.
// Response
{
"templates": [
{
"id": "...",
"name": "Invoice",
"description": "...",
"version": "1.0",
"createdAt": "...",
"updatedAt": "..."
}
]
}Create a new template.
// Request
{ "name": "My Invoice", "description": "...", "template": { "type": "report", ... }, "sampleData": { ... } }
// Response (201)
{ "meta": { "id": "uuid", "name": "My Invoice", ... }, "template": { ... }, "sampleData": { ... } }Load a template and its sample data.
// Response
{ "template": { ... }, "sampleData": { ... } }Update an existing template.
// Request (all fields optional)
{ "name": "Updated Name", "template": { ... }, "sampleData": { ... } }Delete a template.
// Response
{ "success": true }The render pipeline is a 4-stage transformation:
JSON Schema + Data
↓
① Schema Validator (Zod)
↓
② Expression Engine (parse → AST → evaluate)
↓
③ Band Processor (iterate details, resolve expressions)
↓
④ Layout Calculator (absolute positions, pagination)
↓
Intermediate Representation (IR)
↓
Renderer (HTML / PDF / ...)
↓
Output
The IR is renderer-agnostic — pages with positioned, resolved elements. Any renderer can consume it to produce output in any format.
nextreport-engine/
├── apps/
│ └── web/ Next.js application
│ ├── app/ App Router pages + API routes
│ ├── lib/ Template loader, data resolvers
│ └── templates/ Built-in report templates
├── packages/
│ ├── schema/ Zod schemas + JSON Schema export
│ ├── engine-core/ Render pipeline (4 internal modules)
│ │ ├── pipeline/ Orchestrator (public API)
│ │ ├── expression/ Tokenizer, parser, evaluator, functions
│ │ ├── band-processor/ Band iteration + expression resolution
│ │ └── layout/ Position calculation + pagination
│ ├── renderer-html/ IR → HTML
│ ├── renderer-pdf/ HTML → PDF (Puppeteer)
│ ├── canvas/ Canvas abstraction
│ │ ├── ports/ Framework-agnostic interfaces
│ │ └── adapters/konva/ Konva.js implementation
│ ├── ui-designer/ Visual designer React components
│ │ ├── components/ Designer, CanvasArea, Toolbox, etc.
│ │ └── store/ Zustand + Immer (5 slices)
│ └── ui-viewer/ Report viewer component
└── docs/
├── ROADMAP.md Product roadmap
└── superpowers/
├── specs/ Design specifications
└── plans/ Implementation plans
We welcome contributions! NextReportEngine is designed with contribution-friendly boundaries.
1. Add a Component Plugin (beginner-friendly)
Implement the ComponentCoreDefinition interface to add a new report component:
interface ComponentCoreDefinition {
type: string // e.g., "image"
displayName: string // e.g., "Image"
schema: ZodSchema // Validation schema
defaultProperties: Record<string, unknown>
htmlRenderer: (element: ResolvedElement) => string
}Built-in: Text, Table, Image, Barcode. Components needed: Chart, Shape, Divider/Line, Signature.
2. Add an Expression Function
Register a new built-in function:
import { registerFunction } from './registry.js'
registerFunction('avg', (args) => {
const arr = args[0] as unknown[]
const field = String(args[1])
const values = arr.map((item) => Number((item as Record<string, unknown>)[field]))
return values.reduce((a, b) => a + b, 0) / values.length
})Functions needed: avg, min, max, join, substring, replace, round, abs, now.
3. Add a Renderer
Create a new package that consumes the IR (Intermediate Representation):
import type { RenderResult } from '@nextreport/engine-core'
export function renderToCSV(result: RenderResult): string {
// Transform IR pages/elements into CSV format
}Renderers needed: CSV, Excel (xlsx), DOCX, Markdown, plain text.
4. Add a Template
Create a .json template + .data.json sample data in apps/web/templates/. Good templates: receipt, shipping label, report card, certificate, time sheet.
git clone https://github.com/nextreport/engine.git
cd nextreport-engine
pnpm install
pnpm test # Run all 190 tests
pnpm run dev # Start dev server- Engine isolation:
engine-coreand renderers must not import React, Next.js, or Konva - Konva adapter rule: Konva is imported only in
packages/canvas/src/adapters/konva/ - Internal modules:
engine-coreexports onlyrenderReportandvalidateReport— internal modules (expression, band-processor, layout) are not public API - TDD: Write tests first, verify they fail, implement, verify they pass
- ESM: Use
.jsextensions in all TypeScript imports - No over-engineering: YAGNI. Don't add features, abstractions, or error handling for scenarios that don't exist yet
- Fork the repo and create a feature branch
- Write tests for your changes
- Ensure
pnpm testpasses (all 428+ tests) - Ensure
pnpm typecheckpasses - Submit a PR with a clear description
| Layer | Technology |
|---|---|
| Language | TypeScript |
| Framework | Next.js (App Router) |
| UI | React, shadcn/ui, TailwindCSS |
| Monorepo | Turborepo + pnpm workspaces |
| Canvas | Konva.js (via adapter) |
| State | Zustand + Immer |
| Schema | Zod + zod-to-json-schema |
| Puppeteer | |
| Testing | Vitest |
See ROADMAP.md for the full product roadmap.
v0.2 delivered: Image/Barcode components, Group/Page bands, Template CRUD API, Designer UX (drag & drop, resize, keyboard shortcuts).
Coming next:
- Plugin system & component marketplace
- Native PDF generation (no Puppeteer dependency)
- Conditional band visibility (expression-based show/hide)
- Database-backed template storage (PostgreSQL + Prisma)
- AI report generation via MCP server
NextReport Engine — Schema-driven reports for the modern web.
Roadmap •
Contribute •
API Docs