Tutorials / Node.js

Node.js Factur-X & E-Rechnung API

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`.

Prerequisites

  • Node.js 18+
  • axios and form-data packages

Full Implementation

const 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);

NPM Packages vs Docker API

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)

Use Case: Native Fetch API (Node.js 18+, Zero Dependencies)

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
}

Use Case: NestJS Injectable Service

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);
  }
}

Use Case: Validate Before Sending with Express Middleware

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' });
});

Frequently Asked Questions

Does this work with TypeScript?

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.

Can I stream the PDF response to a browser download?

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.

How do I handle the engine being unavailable?

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).

AI Tip

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.