Node.js using NestJS
In this chapter, you'll integrate Oicana into a Node.js web service using NestJS. NestJS is a progressive Node.js framework for building efficient, scalable server-side applications. It uses TypeScript by default and provides a modular architecture. We'll create a simple web service that compiles your Oicana template to PDF and serves it via an HTTP endpoint.
Note
This chapter assumes that you have a working Node.js 18+ setup with npm. If that is not the case, please follow the official Node.js installation guide to install Node.js on your machine.
Let's start with a fresh NestJS project by executing npx @nestjs/cli new oicana-demonpx @nestjs/cli new oicana-demo in a new directory. This will create a new NestJS application with a basic structure. The starter project has a single endpoint defined in the controller. We can test it by starting the service with npm run start:devnpm run start:dev and navigating to http://localhost:3000http://localhost:3000 in a browser.
We will define a new endpoint to compile our Oicana template to a PDF and return the PDF file to the user.
-
Create a new directory in the Node.js project called
templatestemplatesand copyexample-0.1.0.zipexample-0.1.0.zipinto that directory. -
Add the
@oicana/node@oicana/nodenpm package as a dependency withnpm install @oicana/nodenpm install @oicana/node. -
Generate a new controller and service for templates:
npx nest generate module templates
npx nest generate service templates
npx nest generate controller templates
npx nest generate module templates
npx nest generate service templates
npx nest generate controller templates
npx nest generate module templates
npx nest generate service templates
npx nest generate controller templates
npx nest generate module templates
npx nest generate service templates
npx nest generate controller templates
-
Update the templates service to load the template at startup:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, CompilationMode } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(
process.cwd(),
'templates',
'example-0.1.0.zip'
);
const buffer = await fs.readFile(templatePath);
// Template registration defaults to Development mode
// so it will use the development value of our template input
this.template = new Template('example', buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
return this.template.compile(
jsonInputs,
blobInputs,
{ format: 'pdf' },
CompilationMode.Development
);
}
}import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, CompilationMode } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(
process.cwd(),
'templates',
'example-0.1.0.zip'
);
const buffer = await fs.readFile(templatePath);
// Template registration defaults to Development mode
// so it will use the development value of our template input
this.template = new Template('example', buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
return this.template.compile(
jsonInputs,
blobInputs,
{ format: 'pdf' },
CompilationMode.Development
);
}
}import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, CompilationMode } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(
process.cwd(),
'templates',
'example-0.1.0.zip'
);
const buffer = await fs.readFile(templatePath);
// Template registration defaults to Development mode
// so it will use the development value of our template input
this.template = new Template('example', buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
return this.template.compile(
jsonInputs,
blobInputs,
{ format: 'pdf' },
CompilationMode.Development
);
}
}import { Injectable, OnModuleInit } from '@nestjs/common';
import { Template, CompilationMode } from '@oicana/node';
import { promises as fs } from 'fs';
import { join } from 'path';
@Injectable()
export class TemplatesService implements OnModuleInit {
private template: Template;
async onModuleInit() {
const templatePath = join(
process.cwd(),
'templates',
'example-0.1.0.zip'
);
const buffer = await fs.readFile(templatePath);
// Template registration defaults to Development mode
// so it will use the development value of our template input
this.template = new Template('example', buffer);
}
compile(): Uint8Array {
const jsonInputs = new Map();
const blobInputs = new Map();
return this.template.compile(
jsonInputs,
blobInputs,
{ format: 'pdf' },
CompilationMode.Development
);
}
} -
Update the templates controller to add a compile endpoint:
import { Controller, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service';
@Controller('templates')
export class TemplatesController {
constructor(
private readonly templatesService: TemplatesService
) {}
@Post('compile')
compile(@Res() res: Response) {
const pdf = this.templatesService.compile();
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}import { Controller, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service';
@Controller('templates')
export class TemplatesController {
constructor(
private readonly templatesService: TemplatesService
) {}
@Post('compile')
compile(@Res() res: Response) {
const pdf = this.templatesService.compile();
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}import { Controller, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service';
@Controller('templates')
export class TemplatesController {
constructor(
private readonly templatesService: TemplatesService
) {}
@Post('compile')
compile(@Res() res: Response) {
const pdf = this.templatesService.compile();
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}import { Controller, Post, Res } from '@nestjs/common';
import type { Response } from 'express';
import { TemplatesService } from './templates.service';
@Controller('templates')
export class TemplatesController {
constructor(
private readonly templatesService: TemplatesService
) {}
@Post('compile')
compile(@Res() res: Response) {
const pdf = this.templatesService.compile();
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="example.pdf"',
'Content-Length': pdf.length,
});
res.status(200).end(Buffer.from(pdf));
}
}This code defines a new POST endpoint at
/templates/compile/templates/compile. For every request, it compiles the template with empty input maps and returns the PDF file. We explicitly passCompilationMode.DevelopmentCompilationMode.Developmenthere to so the template uses the development value you defined for theinfoinfoinput ("Chuck Norris"). In a follow up step, we will set an input value instead.
After restarting the service, you can test the endpoint with curl:
curl -X POST http://localhost:3000/templates/compile --output example.pdf
curl -X POST http://localhost:3000/templates/compile --output example.pdf
curl -X POST http://localhost:3000/templates/compile --output example.pdf
curl -X POST http://localhost:3000/templates/compile --output example.pdf
The generated example.pdfexample.pdf file should contain your template with the development value.
The PDF generation should not take longer than a couple of milliseconds.
For better performance in production environments with heavy load, consider moving compilation to worker threads. This allows you to offload CPU-intensive compilation work from the main event loop. Libraries like piscina can help with that.
Our compilecompile method is currently calling template.compile()template.compile() with empty input maps and development mode. Now we'll provide explicit input values and switch to production mode:
compile(): Uint8Array {
const jsonInputs = new Map<string, string>();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.compile(jsonInputs, blobInputs);
}
compile(): Uint8Array {
const jsonInputs = new Map<string, string>();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.compile(jsonInputs, blobInputs);
}
compile(): Uint8Array {
const jsonInputs = new Map<string, string>();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.compile(jsonInputs, blobInputs);
}
compile(): Uint8Array {
const jsonInputs = new Map<string, string>();
const blobInputs = new Map();
jsonInputs.set('info', JSON.stringify({ name: 'Baby Yoda' }));
return this.template.compile(jsonInputs, blobInputs);
}
Notice that we removed the explicit CompilationMode.DevelopmentCompilationMode.Development parameter. The compile()compile() method defaults to CompilationMode.ProductionCompilationMode.Production when no mode is specified. Production mode is the recommended default for all document compilation in your application - it ensures you never accidentally generate a document with test data. In production mode, the template will never fall back to development values. If an input value is missing in production mode and the input does not have a default value, the compilation will fail unless your template handles nonenone values for that input.
Calling the endpoint now will result in a PDF with "Baby Yoda" instead of "Chuck Norris". Building on this minimal service, you could set input values based on database entries or the request payload. Take a look at the open source NestJS example project on GitHub for a more complete showcase of the Oicana Node.js integration, including blob inputs, error handling, and Swagger documentation.