---
title: "Getting started — quario"
description: "Build an invoice as a quario definition and render it to a page and to a PDF, from an application record you already have."
url: "https://getquario.com/docs/getting-started"
---

# Getting started

## Installing the engine and a render target

The engine and the targets ship as separate packages. `quario` compiles
definitions and emits an event stream. Each target consumes that stream and
writes one kind of output. A service that only ever returns pages never carries
the PDF typesetter.

Each target declares the engine as a peer dependency. Install `quario`
alongside whichever targets you render with, and add another later without
touching the definitions you already wrote.

```bash title="shell"
npm install quario @quario/html @quario/pdf
```

The packages run on Node 22 or newer. They’re ESM-only, so there is no
CommonJS build to use instead. In the browser they need a bundler that
understands ES2024.

## Creating an instance that carries locale and currency

An instance holds host configuration rather than document content. Locale,
currency, timezone, the license key and the JSONPath budgets all live here,
because each describes the deployment, not the report.

```ts title="report.ts"
import { quario } from 'quario'

const q = quario({ locale: 'en-IE', currency: 'EUR', timeZone: 'UTC' })
```

Presentation policy belongs to the host. The same definition reads as
`€15,076.60` under `en-IE` and as `$15,076.60` under `en-US`, with nothing in
the JSON changing. A definition that hard-coded the symbol would need one copy
per market.

One instance serves every report your service renders. Build it at startup and
keep it. The instance verifies the license key there, once, offline.

## Describing the invoice as a definition

### Starting from the record you already have

The tutorial renders one invoice record from your application. Its shape
owes nothing to quario. It’s the object your ORM or your API already gives
you, with the line items nested under the invoice.

```ts title="the record you already have"
const invoice = {
  number: 'INV-2043',
  issued: '2026-08-14',
  due: '2026-09-13',
  customer: { name: 'Northwind Trading', vatId: 'IE6388047V' },
  lines: [
    { project: 'Rollout', item: 'Discovery workshop', qty: 2, unitPrice: 950, vatRate: 21 },
    { project: 'Rollout', item: 'Integration build', qty: 12, unitPrice: 680, vatRate: 21 },
    { project: 'Support', item: 'Retainer, August', qty: 1, unitPrice: 2400, vatRate: 21 }
  ]
}
```

### Selecting the rows the report walks

A definition is plain JSON. You store it beside your migrations, review it,
and diff it when a customer asks why last month’s invoice looked different.
Expressions in it parse into closures when the definition compiles, so none of
it ever runs as JavaScript source.

`data` is a JSONPath that selects the rows the report walks. Here it reaches
the invoice’s line items, which gives the report three rows. Everything outside
that path stays readable through `$.input`.

### Reading values into the header

An interpolation reads a value into a line of text. `{{ }}` marks one, and the
target escapes whatever comes out of it. quario has no syntax for an unescaped
interpolation. No definition can exclude a data value from escaping.

A value may be a list of runs rather than one string. The issue date is two
runs, `Issued ` and the interpolation, so the cell’s `format` reaches a run
holding a single value. [Keeping amounts as numbers](#keeping-amounts-as-numbers)
says why a format needs one value to speak about.

`$.input` reaches the data you give to `render`. A header draws before any
row, and it reads the whole payload. `$.input.invoice.number` is the invoice
number from the record above. Your application resolves that payload before the
call, and [Supplying the data](https://getquario.com/docs/diving-deeper/supplying-data.md) covers where
it comes from.

A split item places cells across the band. Stacked text items each take a line
of their own, which suits an address block and not an invoice number sitting
opposite an issue date. Each slot takes a `width` as a percentage, and the last
slot takes what’s left.

Every report header item states its own size. Targets give that band a headline
default — bold, at 1.4× the size the document declares — and it sits above the
document’s own `style`. An item declaring no size renders headline-sized on the
page and in the PDF alike. This definition declares `"size": 10`, so its
headline lands at 14.

A split can state that size once for all of its slots. Its own `style` ranks
above the band default, so a split declaring `"size": 9` renders every slot at
9 unless a slot declares otherwise.

```json title="invoice.report.json"
{
  "data": "$.invoice.lines[*]",
  "style": { "family": "sans", "size": 10 },
  "header": [
    {
      "type": "split",
      "slots": [
        { "type": "text", "value": "Invoice No. {{ $.input.invoice.number }}", "width": 60,
          "style": { "size": 18, "bold": true } },
        { "type": "text", "value": [{ "value": "Issued " }, { "value": "{{ $.input.invoice.issued }}" }],
          "style": { "size": 9, "align": "right", "format": "date" } }
      ]
    },
    { "type": "text", "value": "{{ $.input.invoice.customer.name }}",
      "style": { "size": 10, "bold": true, "spaceBefore": 24 } },
    { "type": "text", "value": "VAT {{ $.input.invoice.customer.vatId }}",
      "style": { "size": 9 } }
  ]
}
```

## Turning the line items into a table

A detail band becomes a table when it declares columns. Handed a list of items
instead, quario draws them as stacked bands and repeats them per row. With
`columns` it emits a real table. That reaches HTML as `thead` and `tbody`, the
PDF as a header row repeated on each page, and a workbook as a frozen pane.

`@` is the row the report is currently walking. Inside a column’s `value` it
holds one line item, so `@.qty` is that line’s quantity and
`@.qty * @.unitPrice` is its amount. The arithmetic runs in the expression, where your
reviewers can read it.

```json title="invoice.report.json — detail"
{
  "detail": {
    "columns": [
      { "header": { "value": "Item" }, "value": "{{ @.item }}", "width": 46 },
      { "header": { "value": "Qty", "style": { "align": "right" } },
        "value": "{{ @.qty }}", "width": 10, "style": { "align": "right" } },
      { "header": { "value": "Unit", "style": { "align": "right" } },
        "value": "{{ @.unitPrice }}", "width": 16,
        "style": { "align": "right", "format": "currency" } },
      { "header": { "value": "VAT", "style": { "align": "right" } },
        "value": "{{ @.vatRate / 100 }}", "width": 10,
        "style": { "align": "right", "format": { "kind": "percent", "digits": 0 } } },
      { "header": { "value": "Amount", "style": { "align": "right" } },
        "value": "{{ @.qty * @.unitPrice }}", "width": 18,
        "style": { "align": "right", "format": "currency" } }
    ]
  }
}
```

### Keeping amounts as numbers

`format` presents a bare interpolated number. Interpolate `@.unitPrice` on its
own, declare `"format": "currency"` in its style, and the cell reads €950.00 on
the page while staying the number 950 in a workbook. Formatting it yourself
with `toFixed` would make it a string on every surface. A format applies only
where its run renders one value and nothing else. That’s why the header split
the issue date into runs rather than formatting the whole line.

`format` is a closed kind rather than a pattern string. The four kinds are
`number`, `currency`, `percent`, and `date`, and each carries at most one
modifier. The VAT column declares `{ "kind": "percent", "digits": 0 }` so it
reads 21% instead of 21.00%.

A column’s width is a percentage share of the table. The five columns here add
up to 100. Padding and borders come out of that share rather than adding to it.

## Subtotalling per project and totalling the invoice

### Declaring the aggregates

An aggregate is a reducer name joined to an expression. `sum:=@.qty *
@.unitPrice` runs `sum` over every row the report walks and stores the result
under the name you gave it. The built-in reducers are `sum`, `count`,
`countDistinct`, `avg`, `min`, `max`, `first`, and `last`. `first` and `last`
report the value an expression takes on the row a scope opens and closes on.
They’re the only two that depend on the order the rows arrive in.

A named group puts its own values in scope. Calling the group `project` makes
`project.key` the value it grouped by and `project.lineTotal` its own
aggregate. Report-level aggregates stay on `$`, so `$.subtotal` covers the
whole invoice while `project.lineTotal` covers one project.

### Choosing where a total lands

A total block draws at the foot of the table. Under a group band the table
emits once per group, and the total block goes with it. That makes it the
right place for a per-project subtotal and the wrong place for the invoice
total. Its cells carry `span` to reach across columns, so the row reads as a
label and an amount instead of a line of empty spacers.

The report footer draws once for the whole document. Subtotal, VAT, and total
due belong there, as one grid: three lines laid on the same two columns. The
grid states the widths and each column’s style once, so the amounts align by
construction rather than by three copies of `"width": 82` agreeing. Each row is
a split without its `type`. A row’s `style` carries what that line alone needs,
such as the gap above the subtotal or the rule above the total. A single-project
invoice hides the distinction, because both bands then
draw once.

`round` wraps arithmetic a cell does for itself. Summing money in binary
floating point produces values like 83.75999999999999, which a page presents as
€83.76 while a workbook stores every digit. Wrapping the expression in
`round(…, 2)` settles the stored number as well as the presented one.

```json title="invoice.report.json — groups and totals"
{
  "aggregates": {
    "subtotal": "sum:=@.qty * @.unitPrice",
    "vat": "sum:=@.qty * @.unitPrice * @.vatRate / 100"
  },
  "groups": [
    {
      "name": "project",
      "by": "=@.project",
      "aggregates": { "lineTotal": "sum:=@.qty * @.unitPrice" },
      "header": [
        { "type": "text", "value": "{{ project.key }}",
          "style": { "size": 9, "bold": true, "spaceBefore": 12 } }
      ]
    }
  ],
  "detail": {
    "total": {
      "style": { "align": "right" },
      "rows": [
        { "cells": [
          { "value": "{{ project.key }} subtotal", "span": 4 },
          { "value": "{{ round(project.lineTotal, 2) }}", "style": { "format": "currency" } }
        ] }
      ]
    }
  },
  "footer": [
    {
      "type": "grid",
      "columns": [
        { "width": 82, "style": { "size": 9, "align": "right" } },
        { "style": { "size": 9, "align": "right", "format": "currency" } }
      ],
      "rows": [
        { "style": { "spaceBefore": 16 },
          "slots": [{ "type": "text", "value": "Subtotal" }, { "type": "text", "value": "{{ $.subtotal }}" }] },
        { "slots": [{ "type": "text", "value": "VAT" }, { "type": "text", "value": "{{ round($.vat, 2) }}" }] },
        { "style": { "bold": true, "paddingTop": 5, "borderTopWidth": 0.5, "borderTopStyle": "solid", "borderTopColor": "#1e1f22" },
          "slots": [{ "type": "text", "value": "Total due" }, { "type": "text", "value": "{{ round($.subtotal + $.vat, 2) }}" }] }
      ]
    }
  ]
}
```

## Rendering the definition into a page

### Compiling once and rendering per dataset

`report` compiles the definition once for every later render. Every expression
in the document parses into a closure at that point, which is the expensive
step. `render` then walks your data through those closures. Parsing costs you
once per definition, not once per invoice.

The compiled report is the object your service holds. Compile it at startup, or
the first time something asks for a definition, and keep it. A request then
supplies a dataset and nothing else.

```ts title="render.ts"
import { html } from '@quario/html'

const report = q.report(definition)

const invoice = await getInvoice()
const fragment = await report.render(html(), { invoice })
```

### Dropping the fragment into your page

The HTML target resolves a fragment for your own page shell. It’s a `div` with
class names on it and no document around it. Drop it into a template, an email
body, or a server-rendered response.

The target escapes every interpolation at its own edge. Escaping belongs to the
target, not to the definition, and nothing in the event stream arrives
pre-escaped. Writing `{{{ }}}` is a definition error.

The reference stylesheet is a separate import. The fragment carries no
presentation beyond the styles a definition asked for, which leaves the class
names yours to restyle.

```css title="page.css"
@import '@quario/html/style.css';
```

Unlicensed output carries an evaluation marking. Rendering without a license
key marks the output, and in HTML that’s a banner above the fragment. It’s
the first thing you will see on the first render. Evaluation has no time limit.

## Rendering the same definition as a PDF

The same compiled report renders through a second target. The definition does
not change, the data doesn’t change, and the call differs only in the factory
you give it. Describing a document as data is what buys that.

The PDF target writes the document without a browser. It typesets the pages
itself and writes the file directly. Nothing in the path needs Chromium in
the container or a screenshot step in the middle.

### Setting the page geometry

Paper can come from either side. `pdf({ page })` takes a named size or a pair
of points, along with a margin, and a definition can declare its own
`page.size`, `page.orientation` and `page.margin`. Leave the paper to the host
when one definition prints on A4 for one customer and Letter for the next.
Declare it when the document only works one way, like a landscape statement.
Each key comes from one side or the other, and declaring the same key in both
is a render error rather than a silent override.

Every factory reads its options once, at the call, and refuses the ones it
doesn’t know — `quario()` as much as `pdf()`. An unrecognised key, a key of the
wrong type, or an options value that isn’t an object throws a `TypeError`
naming the path — `options.page.size`. A typo then costs one stack trace where
you wrote it, rather than the option you meant to set. `quario({ licence: … })`
throws at the call. It doesn’t quietly watermark every document. Nullish is
absence at either level, so `{ meta: null }` and a `meta` whose one property is
`undefined` are both properties you didn’t write.

The key sets differ by target, because the formats do. `pdf()` takes `page`,
`meta` and `fonts`. `docx()` takes `page` and `meta`. `xlsx()` takes `page`,
`meta` and `filter`. `html()` takes `fonts` and `paths`. `csv()` takes
nothing. So one options object shared across several targets holds only as
long as every one of them knows every key in it.

A page band draws on every page. `page.footer` sits on the page’s baseline
instead of following the last row. Inside it, `{{ page.number }}` and `{{
page.total }}` are readable. Targets that don’t paginate ignore the band.

```json title="invoice.report.json — page band"
{
  "page": {
    "footer": [
      { "type": "text", "value": "{{ $.input.invoice.number }} · page {{ page.number }} of {{ page.total }}",
        "style": { "size": 8, "align": "right" } }
    ]
  }
}
```

```ts title="render.ts"
import { pdf } from '@quario/pdf'

const bytes = await report.render(
  pdf({ page: { size: 'A4', margin: 54 }, meta: { title: 'Invoice INV-2043' } }),
  { invoice }
)
```

The render resolves a `Uint8Array`. Write it to disk, put it in object storage,
or send it as a response body. The bytes are deterministic. One definition and one dataset
produce the same file every time.

## Catching a broken definition before it ships

`plan` compiles a definition without rendering anything. It returns the
compiled report, or null when the document has problems, along with the
problems themselves. Running it over the definitions you ship is a test that
needs no data.

`plan` reports warnings beside problems. A warning is a declaration the engine
will quietly drop, such as a `format` on a line that mixes text and a value. The
definition still compiles, so a warning never blocks a build on its own. It’s
what would have caught the header’s issue date had it stayed one string.

A problem names the schema path it sits at. `detail.columns[3].value` tells you
which cell to open, and the message carries the offending source beside it.
`plan` returns the problems as a list, and a failing check prints all of them
at once.

```ts title="check.ts"
const { report, problems, warnings } = q.plan(definition)

for (const problem of problems) console.error(`${problem.path}: ${problem.message}`)
for (const warning of warnings) console.warn(`${warning.path}: ${warning.message}`)

if (!report) process.exitCode = 1
```

A definition that stops compiling is the failure that hides longest. Definitions
sit outside your type system, so an engine upgrade that changes a shape breaks
them quietly until someone renders one. This site taught a superseded
`detail.total` shape in two guides for a full release before a build-time check
caught it.

A document may also state which declarations it can’t do without, and have
`plan` check them against the targets you render with. A target that withdraws
a declaration does so silently, so this is how you say that one of them
matters. [Validating definitions](https://getquario.com/docs/diving-deeper/validating.md) carries it.

```json
{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://getquario.com/"},{"@type":"ListItem","position":2,"name":"Docs","item":"https://getquario.com/docs"},{"@type":"ListItem","position":3,"name":"Getting started","item":"https://getquario.com/docs/getting-started"}]}
```
