Calco

Node.js / TypeScript

The Calco client is a zero-dependency connector built on the native fetch and FormData APIs. It works in any Node.js 18+ runtime and ships full TypeScript types.

Add the connector

There's no npm package to install (yet) — the client is a single, self-contained TypeScript file. Copy index.ts from the Node connector into your project as calco/index.ts (or add a path alias) so the calco imports below resolve:

ts
import { CalcoClient } from "calco";

Quick start

ts
import { CalcoClient } from "calco";
import fs from "node:fs";

const client = new CalcoClient("http://localhost:8080", {
  apiKey: "dc_live_...",
});

// Convert a local XLSX file to PDF
const pdf = await client.convert("./invoice.xlsx", { format: "pdf" });
await fs.promises.writeFile("invoice.pdf", pdf);

The constructor takes the base URL and an optional { apiKey }. The key is sent as Authorization: Bearer <key> on every request.

Converting a file

convert(filePath, options?) uploads a local file and returns the rendered bytes as a Buffer. No template is stored.

ts
const buf = await client.convert("./template.xlsx", {
  format: "pdf",          // "pdf" | "html" | "csv" | "ods" | "xls"
  dataJson: JSON.stringify({ company: { name: "Acme" } }),
  sheet: "Sheet1",
  title: "Invoice #42",
});

Extract the placeholder map from a template with scaffold:

ts
const scaffold = await client.scaffold("./template.xlsx");
// { company: { name: "name0", address: "address0" }, items: ... }

Managing templates

Upload a template once, then render it repeatedly by id and version.

ts
// Create
const template = await client.createTemplate("./invoice.xlsx", {
  name: "Invoice Template",
  category: "billing",
  tags: "invoice,billing,finance",
});

// List
const templates = await client.listTemplates();

// Update metadata
await client.updateTemplate(template.id, { name: "Updated Invoice" });

// Add a new version
const { version } = await client.addVersion(template.id, "./invoice-v2.xlsx");

// Delete (removes all versions)
await client.deleteTemplate(template.id);

Rendering a stored template

ts
const pdf = await client.renderTemplate(
  template.id,
  1, // version
  {
    company: { name: "Acme Corp" },
    invoice: { number: "INV-001", total: "1,250.00" },
  },
  { format: "pdf", title: "Invoice INV-001" },
);

await fs.promises.writeFile("output.pdf", pdf);

Batch rendering

Render one document per record and receive a ZIP archive:

ts
const zip = await client.batchRender(
  template.id,
  [
    { name: "Alice", amount: "100" },
    { name: "Bob", amount: "200" },
  ],
  { format: "pdf", titleTemplate: "report-{{index}}-of-{{n}}", sheet: "Sheet1" },
);

await fs.promises.writeFile("batch.zip", zip);

By-hash workflow

Every stored version exposes a content hash (sha256 of the file). Address a version directly by hash — no id or version number required:

ts
const meta = await client.getTemplateByHash("3c9f...d1ab");
const pdf = await client.renderByHash("3c9f...d1ab", { customer: "Jane" }, { format: "pdf" });
const xlsx = await client.downloadByHash("3c9f...d1ab", { format: "xlsx" });

Error handling

Non-2xx responses throw CalcoError, which carries the HTTP status and body:

ts
import { CalcoError } from "calco";

try {
  await client.convert("./bad-file.xlsx");
} catch (err) {
  if (err instanceof CalcoError) {
    console.error(`Status: ${err.status}`);
    console.error(`Body: ${err.body}`);
  }
}

Framework integration

Express

ts
import express from "express";
import { CalcoClient } from "calco";

const app = express();
const dc = new CalcoClient("http://localhost:8080");

app.get("/api/invoice/:id", async (req, res) => {
  const data = await getInvoiceData(req.params.id); // your DB call
  const pdf = await dc.renderTemplate("invoice-tpl-123", 1, data, {
    format: "pdf",
    title: `Invoice ${req.params.id}`,
  });
  res.setHeader("Content-Type", "application/pdf");
  res.setHeader("Content-Disposition", `attachment; filename="invoice-${req.params.id}.pdf"`);
  res.send(pdf);
});

app.listen(3000);

NestJS

ts
import { Injectable } from "@nestjs/common";
import { CalcoClient } from "calco";

@Injectable()
export class DocumentService {
  private readonly client = new CalcoClient(
    process.env.CALCO_URL ?? "http://localhost:8080",
  );

  generateInvoicePdf(data: Record<string, unknown>): Promise<Buffer> {
    return this.client.renderTemplate("invoice-tpl-123", 1, data, { format: "pdf" });
  }
}

Next.js API route (App Router)

ts
// app/api/invoice/[id]/route.ts
import { CalcoClient } from "calco";

const dc = new CalcoClient(process.env.CALCO_URL!);

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const data = await getInvoiceData(params.id);
  const pdf = await dc.renderTemplate("invoice-tpl-123", 1, data, {
    format: "pdf",
    title: `Invoice ${params.id}`,
  });

  return new Response(pdf, {
    headers: {
      "Content-Type": "application/pdf",
      "Content-Disposition": `attachment; filename="invoice-${params.id}.pdf"`,
    },
  });
}

Exported types

ts
import type {
  Template,
  TemplateVersion,
  OutputFormat,
  ConvertOptions,
  CreateTemplateOptions,
  UpdateTemplateFields,
  RenderTemplateOptions,
  BatchRenderOptions,
} from "calco";