Skip to content

Recipes

Transactional email with MJML + a layout

typescript
// Shell (MJML) — note {{ content }} where the body goes.
await layouts.createTemplateLayout({
  name: 'mjml-shell',
  displayName: 'MJML shell',
  engine: TemplateEngineEnum.NUNJUCKS,
  language: TemplateLanguageEnum.MJML,
  content: `<mjml><mj-body>
    <mj-section><mj-column>
      <mj-text font-size="20px">{{ brandName }}</mj-text>
    </mj-column></mj-section>
    {{ content }}
  </mj-body></mjml>`,
});

// Body template — rendered, then injected into the shell, then MJML -> HTML.
await templates.createTemplate({
  name: 'welcome-email',
  displayName: 'Welcome',
  templateLayoutName: 'mjml-shell',
  engine: TemplateEngineEnum.NUNJUCKS,
  language: TemplateLanguageEnum.MJML,
  subject: 'Welcome, {{ firstName }}!',
  content: `<mj-section><mj-column>
    <mj-text>Hello {{ firstName }}, thanks for joining {{ brandName }}.</mj-text>
  </mj-column></mj-section>`,
});

const { subject, content } = await templates.render({
  name: 'welcome-email',
  context: { firstName: 'Ada', brandName: 'Acme' },
});
// `content` is bulletproof email HTML — hand it to your mailer.

Per-tenant branding override

typescript
const system = await templates.findTemplate('welcome-email', 'system');

await templates.overwriteSystemTemplate(system.id, {
  scope: 'tenant',
  scopeId: tenant.id,
  content: `<mj-section><mj-column>
    <mj-text>Welcome to {{ tenantName }}, {{ firstName }}!</mj-text>
  </mj-column></mj-section>`,
});

// Rendering for this tenant now uses the override; everyone else gets the system default.
await templates.render({ name: 'welcome-email', scope: 'tenant', scopeId: tenant.id, context });

Reusable filters (dates, money)

typescript
NestDynamicTemplatesModule.forRoot({
  engines: { template: ['njk'], language: ['html'] },
  filters: {
    formatDate: (d: Date, fmt = 'YYYY-MM-DD') => /* your formatter */ '',
    formatCurrency: (n: number, ccy = 'USD') =>
      new Intl.NumberFormat('en-US', { style: 'currency', currency: ccy }).format(n),
  },
});
nunjucks
Order {{ orderId }} placed {{ date | formatDate('MMMM D, YYYY') }} — total {{ amount | formatCurrency('USD') }}

Surfacing render errors to an API client

Because render errors are HTTP exceptions, a controller can simply let them bubble:

typescript
@Post('preview')
async preview(@Body() dto: PreviewDto) {
  // Throws TemplateRenderError (422) with full details if a variable is missing —
  // the client receives a precise JSON body, no extra handling needed.
  return this.templates.render(dto);
}

See Error handling for the response shape.

Released under the MIT License.