Skip to Content
GuidesBackend Rendering

Backend Rendering

ReforgioServer works in any Node.js backend — Express, Fastify, NestJS, Next.js server actions, tRPC procedures, Hono. The only rule: it runs on the server. The secret key grants full access to your organization and must never reach a browser.

import { ReforgioServer } from '@reforgio/sdk-core/server' const reforgio = new ReforgioServer(process.env.REFORGIO_SECRET_KEY!)

The constructor takes optional settings:

new ReforgioServer(secretKey, { userId?: string // attribute renders to an end user (shows up in usage per user) apiUrl?: string // override the API base URL (e.g. a local API in development) })

The render call

render(templateId: string, data: Record<string, unknown>, format?: 'pdf' | 'xlsx'): Promise<RenderResponse>
  • data keys must match the template’s variable names; an items-table variable takes an array of row objects.
  • format defaults to 'pdf'. XLSX requires the template to have the format enabled and at least one table section — otherwise the API answers 400 No table sections found.

Pattern: Next.js server action

Complete, safe-to-copy version (this is the pattern the demo app  uses in production):

app/actions.ts
'use server' import { ReforgioServer, ReforgioError } from '@reforgio/sdk-core/server' const reforgio = new ReforgioServer(process.env.REFORGIO_SECRET_KEY!) export type RenderResult = | { success: true; downloadUrl: string; renderTimeMs: number } | { success: false; error: string } export async function generateInvoice(data: { company_name: string customer_name: string items: Array<{ name: string; quantity: number; price: number }> }): Promise<RenderResult> { try { const doc = await reforgio.render('YOUR_TEMPLATE_ID', data) return { success: true, downloadUrl: doc.downloadUrl, renderTimeMs: doc.renderTimeMs } } catch (err) { if (err instanceof ReforgioError) { return { success: false, error: err.message } } throw err } }

Call it from a client component via useTransition or a form action. For Express/Fastify/NestJS, the body of the function is identical — only the wrapping changes (route handler instead of server action).

Never rethrow ReforgioError raw into your UI without handling — but do rethrow unknown errors (network failures, bugs) so they hit your error monitoring instead of being swallowed.

Error handling

Every non-2xx API response throws a ReforgioError with the server’s exact message and HTTP status:

try { await reforgio.render(templateId, data) } catch (err) { if (err instanceof ReforgioError) { if (err.statusCode === 429) { // rate limited — 100 requests/min per API key; back off and retry } else if (err.statusCode === 404) { // 'Template not found' } else if (err.statusCode === 400) { // 'Template is not published' | 'No table sections found' | validation details } } }

The complete message catalog is in Errors.

Download URLs

downloadUrl in the render response is a presigned storage URL:

  • It’s time-limited (about 1 hour by default). Fine for “render → hand to the user right now”.
  • Don’t store it. Store documentId instead, and get a fresh link any time with GET /v1/downloads/:id — the endpoint answers 302 redirecting to a new presigned URL, so browsers and fetch follow it transparently.
  • 404 Document not found from downloads means a wrong ID, another org’s document, or a render that never completed.

Beyond rendering

The server client can also read your template catalog:

const templates = await reforgio.getTemplates() // all non-archived templates const template = await reforgio.getTemplate(id) // one template, with sections & variables

Publishing, archiving, versions, usage reports, and key rotation are available over REST with the same x-api-key auth.

Rate limit: 100 requests per minute per API key. Exceeding it returns 429. If you render in bulk, queue and pace your calls.