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>datakeys must match the template’s variable names; anitems-tablevariable takes an array of row objects.formatdefaults to'pdf'. XLSX requires the template to have the format enabled and at least one table section — otherwise the API answers400 No table sections found.
Pattern: Next.js server action
Complete, safe-to-copy version (this is the pattern the demo app uses in production):
'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
documentIdinstead, and get a fresh link any time withGET /v1/downloads/:id— the endpoint answers302redirecting to a new presigned URL, so browsers andfetchfollow it transparently. 404 Document not foundfrom 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 & variablesPublishing, 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.