A robust alternative to fragile factur-x NPM packages. Integrate the XRechnung and Factur-X Engine API into NestJS, Express, or serverless functions using `axios`.
axios and form-data packagesconst axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');
async function convertToFacturX(pdfPath, metadata) {
const form = new FormData();
form.append('pdf', fs.createReadStream(pdfPath));
form.append('metadata', JSON.stringify(metadata));
try {
const response = await axios.post('http://localhost:8000/v1/convert', form, {
headers: {
...form.getHeaders(),
},
responseType: 'arraybuffer', // Critical for binary PDF download
});
fs.writeFileSync('output-factur-x.pdf', response.data);
console.log('Success: Invoice saved to output-factur-x.pdf');
} catch (error) {
console.error('API Error:', error.response ? error.response.data.toString() : error.message);
}
}
// Example usage
const myMetadata = {
invoice_number: "NODE-42",
seller: { name: "JS Services" },
totals: { net_amount: 50.00 }
};
convertToFacturX('./input.pdf', myMetadata);
There is no mature NPM package that handles the full EN 16931 Schematron validation stack. Existing packages generate XML but skip the business-rule validation required by French, German, and Belgian mandates. The Docker API approach keeps Node.js dependency-free while the engine handles Saxon-HE and VeraPDF internally.
| Approach | Schematron | NPM deps | PDF/A-3 |
|---|---|---|---|
| factur-x (npm) | None | xml2js, pdflib | Partial |
| CLI wrappers (Mustang) | Yes | child_process + JVM | Yes |
| Factur-X Engine API | Yes (Saxon-HE) | axios or fetch only | Yes (VeraPDF) |
Node.js 18 ships with a built-in fetch and FormData. No axios
or form-data packages needed:
import { readFileSync } from 'fs';
async function generateInvoice(pdfPath, metadata) {
const form = new FormData();
form.append('pdf', new Blob([readFileSync(pdfPath)]), 'invoice.pdf');
form.append('metadata', JSON.stringify(metadata));
const res = await fetch('http://localhost:8000/v1/convert', {
method: 'POST',
body: form,
});
if (!res.ok) throw new Error(await res.text());
return Buffer.from(await res.arrayBuffer()); // binary PDF
}
Wrap the API call in a NestJS @Injectable() service. Inject HttpService
(from @nestjs/axios) and return the PDF buffer directly to the controller:
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
import * as FormData from 'form-data';
import * as fs from 'fs';
@Injectable()
export class InvoiceService {
constructor(private readonly http: HttpService) {}
async generateFacturX(pdfPath: string, metadata: object): Promise<Buffer> {
const form = new FormData();
form.append('pdf', fs.createReadStream(pdfPath));
form.append('metadata', JSON.stringify(metadata));
const { data } = await firstValueFrom(
this.http.post('http://facturx-engine:8000/v1/convert', form, {
headers: form.getHeaders(),
responseType: 'arraybuffer',
}),
);
return Buffer.from(data);
}
}
Add a validation gate in your Express upload route. Reject non-compliant invoices before they reach your ERP or document archive:
const express = require('express');
const multer = require('multer');
const FormData = require('form-data');
const axios = require('axios');
const upload = multer({ storage: multer.memoryStorage() });
const app = express();
app.post('/invoices/upload', upload.single('invoice'), async (req, res) => {
const form = new FormData();
form.append('file', req.file.buffer, req.file.originalname);
const { data } = await axios.post(
'http://localhost:8000/v1/validate', form,
{ headers: form.getHeaders() }
);
if (!data.is_valid) {
return res.status(422).json({ errors: data.errors });
}
// proceed to archive / ERP injection
res.json({ status: 'accepted' });
});
Yes. The API is language-agnostic. Use axios
with AxiosResponse<ArrayBuffer> typing or the native fetch
with Response.arrayBuffer(). No type definitions for the engine are needed —
the JSON metadata payload is a plain object.
Yes. Set responseType: 'stream' in axios and
pipe the response directly to res in Express:
apiResponse.data.pipe(res). Set
Content-Disposition: attachment; filename="invoice.pdf" on your response headers.
Wrap calls in a try/catch and check GET /health
first if you need a pre-flight check. For production, use Docker Compose healthchecks to
ensure the engine is ready before your Node.js service starts (depends_on: condition: service_healthy).
When using Copilot or ChatGPT to scaffold your integration, paste the
openapi.json
into context. It ensures the AI correctly generates the multipart/form-data
pattern and arraybuffer response handling.