npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ackplus/nest-dynamic-templates

v2.0.0

Published

A powerful and flexible dynamic template rendering library for NestJS applications with support for Nunjucks, Handlebars, EJS, Pug, MJML, Markdown, and more.

Readme

@ackplus/nest-dynamic-templates

Database-backed, multi-engine template rendering for NestJS — with error messages that actually tell you what went wrong.

Store templates in your database, render them at runtime with the engine of your choice (Nunjucks, Handlebars, EJS, Pug), and post-process the output as HTML, MJML, Markdown or plain text. Built for transactional email, notifications, PDFs and any per-tenant/per-locale content.

📖 Full documentation: https://ack-solutions.github.io/nest-dynamic-templates/


Why this library

  • 🧩 Two-stage pipeline — a template engine interpolates your variables, then a language processor turns the result into final markup. Mix and match (e.g. Nunjucks → MJML).
  • 🗄️ Database storage — templates and layouts live in your DB (TypeORM), versioned and editable at runtime.
  • 🌍 Scope & locale resolution — ship system defaults and let a user/tenant/organization override them, per locale, with automatic fallback.
  • 🪶 Lightweight & lazy — only the engines you enable are loaded, so you only install the peers you actually use.
  • 🛑 Diagnostic errors — when a render fails you get the missing variable, the source line, the context keys you passed, and an actionable hint — not a generic 500.

Install

npm install @ackplus/nest-dynamic-templates
# peer deps you always need:
npm install @nestjs/typeorm typeorm reflect-metadata

Then install only the engines you enable:

| You enable | Install | | --- | --- | | njk (Nunjucks, default) | npm install nunjucks | | hbs (Handlebars) | npm install handlebars | | ejs | npm install ejs | | pug | npm install pug | | mjml (email) | npm install mjml | | md (Markdown) | npm install marked | | html, txt | nothing — built in |

Quick start

1. Register the module

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import {
  NestDynamicTemplatesModule,
  TemplateEngineEnum,
  TemplateLanguageEnum,
} from '@ackplus/nest-dynamic-templates';

@Module({
  imports: [
    TypeOrmModule.forRoot({
      type: 'postgres',
      // ...your db config
      autoLoadEntities: true, // picks up the library's entities automatically
    }),
    NestDynamicTemplatesModule.forRoot({
      isGlobal: true,
      engines: {
        template: [TemplateEngineEnum.NUNJUCKS],
        language: [TemplateLanguageEnum.HTML, TemplateLanguageEnum.MJML],
      },
    }),
  ],
})
export class AppModule {}

Prefer explicit entities (e.g. for migrations)? Import NestDynamicTemplatesEntities and spread it into your TypeORM entities array.

2. Create a template

import { Injectable } from '@nestjs/common';
import {
  TemplateService,
  TemplateEngineEnum,
  TemplateLanguageEnum,
} from '@ackplus/nest-dynamic-templates';

@Injectable()
export class TemplatesSeeder {
  constructor(private readonly templates: TemplateService) {}

  seed() {
    return this.templates.createTemplate({
      name: 'welcome-email',
      displayName: 'Welcome email',
      scope: 'system',
      locale: 'en',
      subject: 'Welcome, {{ firstName }}!',
      content: '<h1>Hello {{ firstName }}</h1><p>Thanks for joining.</p>',
      engine: TemplateEngineEnum.NUNJUCKS,
      language: TemplateLanguageEnum.HTML,
      type: 'email',
    });
  }
}

3. Render it

const { subject, content } = await this.templates.render({
  name: 'welcome-email',
  scope: 'system',
  locale: 'en',
  context: { firstName: 'Ada' },
});
// subject -> "Welcome, Ada!"
// content -> "<h1>Hello Ada</h1><p>Thanks for joining.</p>"

Need to render a raw string without touching the DB? Use renderContent():

const html = await this.templates.renderContent({
  content: 'Hi {{ name }}',
  engine: TemplateEngineEnum.NUNJUCKS,
  language: TemplateLanguageEnum.HTML,
  context: { name: 'World' },
});

Error handling

This is the headline feature. Every render failure throws a typed error with a structured details payload and the original error attached as cause. Missing-variable failures name the variable and list the context you actually passed:

import { TemplateRenderError, TemplateErrorCode, isTemplateError } from '@ackplus/nest-dynamic-templates';

try {
  await templates.render({ name: 'welcome-email', context: { email: '[email protected]' } });
} catch (err) {
  if (isTemplateError(err)) {
    console.error(err.code);             // 'TEMPLATE_RENDER_FAILED'
    console.error(err.details.missingVariable); // 'firstName'
    console.error(err.details.contextKeys);     // ['email']
    console.error(err.details.location);        // { line: 1, column: 16 }
    console.error(err.details.snippet);         // '<h1>Hello {{ firstName }}</h1>...'
    console.error(err.details.hint);            // 'Pass "firstName" in the render context. ...'
  }
}

The thrown message reads:

Failed to render template "welcome-email" [njk → html, scope=system, locale=en]: variable "firstName" is undefined.

Each error also extends the matching NestJS HTTP exception, so Nest's exception filter maps the right status automatically and your existing catch (NotFoundException) keeps working:

| Error | HTTP | code | | --- | --- | --- | | TemplateRenderError | 422 | TEMPLATE_RENDER_FAILED | | TemplateNotFoundError | 404 | TEMPLATE_NOT_FOUND | | TemplateInputError | 400 | TEMPLATE_INVALID_INPUT | | TemplateForbiddenError | 403 | TEMPLATE_FORBIDDEN | | TemplateConflictError | 409 | TEMPLATE_CONFLICT | | TemplateEngineUnavailableError | 500 | TEMPLATE_ENGINE_UNAVAILABLE |

Configuration

NestDynamicTemplatesModule.forRoot({
  isGlobal: true,

  // Only these engines are loaded. Default: { template: ['njk'], language: ['html','mjml','txt'] }
  engines: {
    template: [TemplateEngineEnum.NUNJUCKS, TemplateEngineEnum.HANDLEBARS],
    language: [TemplateLanguageEnum.HTML, TemplateLanguageEnum.MJML, TemplateLanguageEnum.MARKDOWN],
  },

  // Custom filters/helpers — available in EVERY template engine.
  filters: {
    formatDate: (d: Date, fmt: string) => /* ... */ '',
    formatCurrency: (n: number, ccy: string) =>
      new Intl.NumberFormat('en-US', { style: 'currency', currency: ccy }).format(n),
  },

  // Global values injected into every render (strings, numbers, objects or functions).
  globals: {
    brandName: 'Acme',
    year: () => new Date().getFullYear(),
  },

  // Raw options forwarded to the underlying engine libraries.
  engineOptions: {
    template: { njk: { autoescape: true, trimBlocks: true } },
    language: { mjml: { validationLevel: 'soft' } },
  },
});

Async configuration

NestDynamicTemplatesModule.forRootAsync({
  isGlobal: true,
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    engines: { template: ['njk'], language: ['html', 'mjml'] },
    globals: { appUrl: config.get('APP_URL') },
  }),
});

Scope & locale resolution

render() resolves a template by trying, in order:

  1. requested scope + requested locale
  2. requested scope + en
  3. system scope + requested locale
  4. system scope + en

This lets you ship system defaults and override them per tenant/user and per language. Only active templates (isActive: true) are resolved. Example:

// Falls back to the system template if this user has no override.
await templates.render({ name: 'welcome-email', scope: 'user', scopeId: userId, locale: 'fr' });

Template fields

A template is a database row. The key fields (full reference with use cases in the docs):

| Field | Required | Default | Purpose | | --- | --- | --- | --- | | name | ✅ | — | Stable id you render by, e.g. welcome-email. | | scope | — | system | Who owns this versionsystem (shared default) or a custom owner kind like tenant/user. | | scopeId | — | null | Which owner inside the scope (e.g. the tenant id). Empty for system. | | locale | — | en | Language variant; missing locales fall back to en. | | engine | — | njk | Template engine: njk, hbs, ejs, pug. | | language | — | null | Output processor: html, mjml, md, txt. | | subject | — | null | Subject line (emails); rendered with the engine. | | content | ✅ | — | The template body. | | templateLayoutName | — | null | Layout to wrap this content in. | | displayName | — | — | Human-friendly label for admin UIs. | | type | — | null | Free category for grouping, e.g. email/sms. | | previewContext | — | null | Sample data for previews (not used at render time). | | isActive | — | true | Set false to disable without deleting. |

scope vs scopeId is the part to understand: scope is the kind of owner (system, or your own tenant/user/…), and scopeId is the exact owner (the tenant id). Together with name + locale they uniquely identify a template, and a render falls back from a specific override to the system default — so you only store overrides where they differ.

Layouts

Layouts are reusable wrappers (e.g. an email shell). The child content is injected where the layout references {{ content }}:

await layouts.createTemplateLayout({
  name: 'email-shell',
  displayName: 'Email shell',
  engine: TemplateEngineEnum.NUNJUCKS,
  language: TemplateLanguageEnum.HTML,
  content: '<html><body><header>{{ brandName }}</header>{{ content }}</body></html>',
});

// Attach it to a template:
await templates.createTemplate({
  name: 'welcome-email',
  templateLayoutName: 'email-shell',
  /* ...rest... */
});

Services

  • TemplateServicerender, renderContent, createTemplate, updateTemplate, overwriteSystemTemplate, deleteTemplate, getTemplates, findTemplate, getTemplateById.
  • TemplateLayoutService — the same surface for layouts.
  • TemplateConfigService — read-only accessor over the resolved config.
  • TemplateEngineRegistryService — access the engine instances directly.

Migrating from v1

v2 is a focused redesign. See MIGRATION.md for the (short) list of breaking changes and how to update — most apps only need to rename enginesOptions to the new flat filters / globals / engineOptions.

License

MIT © AckPlus