node
backend
nest.js
email
alpha

Liquidjs - Template engine

Author: Łukasz Malinowski

Problem context

Creating a dynamic bridge between an HTML file and a data store is a common problem faced by developers. This is where template engines come in handy. In this article, we will focus on the use of a template engine to create dynamic templates for an email sending service. The use of a template engine enables us to access variables from within a template, allowing us to create dynamic, personalized emails that are driven by data from our data store. By utilizing a template engine, we can streamline the process of creating and sending emails, making it easier and more efficient for developers to create dynamic, data-driven email content.

LiquidJS

Liquid - Safe, customer-facing template language for flexible web apps. Liquid is an open-source template language created by Shopify and written in Ruby. It is the backbone of Shopify themes and is used to load dynamic content on storefronts. Liquid has been in production use at Shopify since 2006 and is now used by many other hosted web applications. https://shopify.github.io/liquid/

LiquidJS provides a simple, yet powerful syntax for creating dynamic templates for web and email content. This syntax, known as Liquid, is widely used across the web, especially in Jekyll sites, Github pages, and Shopify templates, making LiquidJS a popular choice for developers looking for a familiar and efficient templating solution. With its easy-to-learn syntax, LiquidJS allows developers to create dynamic templates quickly and efficiently, without having to worry about complex logic or data manipulation. Additionally, LiquidJS is highly customizable, allowing developers to extend the engine with custom tags, filters, and variables to meet their specific needs.

Pros:
Cons:

Popular alternative template engines:

Rest of engines have less than 1k stars on github.

Installation and setup

First of all lets install liquidjs in our project

npm install liquidjs

Most simple use case is to parse and render:

import { Liquid } from "liquidjs";

const engine = new Liquid();

engine
  .parseAndRender("{{name | capitalize}}", { name: "alice" })
  .then(console.log); // outputs 'Alice'

Example usecase - email module in NestJS

Directory structure for this example
email
│
├── email.module.ts
├── email.service.ts
├── template.service.ts
└── template
    ├── layouts
    │   ├── default.liquid
    │   └── plain.liquid
    └── views
        ├── reset-password-request.liquid
        └── reset-password.liquid

email.module.ts
import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";

import { EmailService } from "./email.service";
import { TemplateService } from "./template.service";

@Module({
  imports: [ConfigModule],
  providers: [
    {
      useFactory: (config: ConfigService) => {
        return new TemplateService(
          `${config.get("app.workingDirectory")}/src/email`
        );
      },
      provide: TemplateService,
      inject: [ConfigService],
    },
    EmailService,
  ],
})
export class EmailModule {}

Code above is defining EmailModule. The purpose of this module is to provide the functionality for sending emails, including the rendering of email templates. The TemplateService is created using a factory method that is specified in the useFactory property. This factory method takes in a ConfigService instance and returns a new TemplateService instance, passing in a string that is constructed using the workingDirectory configuration value. In this example workingDirectory is equal to process.env.PWD || process.cwd()

template.service.ts
import { Liquid } from "liquidjs";
import { join } from "path";

export class TemplateService {
  private readonly engine: Liquid;

  constructor(resourcesPath: string) {
    this.engine = new Liquid({
      root: join(resourcesPath, "template"),
      extname: ".liquid", // if you set extname then you don't have to specify extension while importing partials
    });
  }

  public async renderFile(filename: string, ctx?: Record<string, unknown>) {
    return this.engine.renderFile("views/" + filename, ctx);
  }
}

This code defines a TemplateService class that uses the LiquidJS library to render Liquid templates. The class has a renderFile method that takes in two parameters: filename, which is the name of the file to render, and ctx, which is an optional context object that can be used to pass data to the template.

email.service.ts
import { Injectable, Logger } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import * as nodemailer from "nodemailer";
import SMTPTransport from "nodemailer/lib/smtp-transport";

import { TemplateService } from "./template.service";

type EmailSend = {
  email: string;
  subject: string;
  payload: any;
  template: string;
};

@Injectable()
export class EmailService {
  private readonly logger = new Logger(EmailService.name);
  private transporter: nodemailer.Transporter;

  constructor(
    private readonly configService: ConfigService,
    private readonly templateService: TemplateService
  ) {
    this.transporter = nodemailer.createTransport({
      host: configService.get("mail.host"),
      port: configService.get("mail.port"),
      secure: configService.get("mail.secure"),
      service: configService.get("mail.service"),
      auth: {
        user: configService.get("mail.user"),
        pass: configService.get("mail.password"),
      },
      tls: { rejectUnauthorized: false },
      ignoreTLS: true,
    } as SMTPTransport.Options);
  }

  async send({ email, subject, payload, template }: EmailSend) {
    const compiledTemplate = await this.templateService.renderFile(
      template,
      payload
    );

    this.transporter
      .sendMail({
        from: this.configService.get("mail.from"),
        to: email,
        subject,
        html: compiledTemplate,
      })
      .catch((error) => {
        this.logger.error("Sending mail has failed:", error);
      });
  }
}

EmailService class is responsible for sending emails using the Nodemailer library. In the send method, the templateService.renderFile method is called to render the template specified in the template property of the EmailSend object with the data from the payload property.

Liquid files

layouts/default.liquid
<html>
  <head>
    <style>
      // some fancy styling for actual layout
    </style>
  </head>
  <body>
    // view will use `block content` to render here itself
    {% block content %} {%endblock %}
  </body>
</html>
partials/button.liquid
<a href="{{ link }}">{{ text }}</a>
views/reset-password-request.liquid
// layout is defining which layout we want to use, then we put content in
`block.content`
{% layout "layouts/default" %}

{% block content %}

    // capitalize is built-in function
    <p>Hi {{ firstname | capitalize }},</p>
    <p>You requested to reset your password.</p>
    <p>Please, click the link below to reset your password</p>

    // here we are including button partial and passing `text` and link into it
    // commas are very important here
    {% include "partials/button", text: 'Reset Password', link: link %}

{% endblock %}

Finally

this.emailService.send({
  email: event.email,
  subject: "Reset password requested",
  payload: {
    firstname,
    link,
  },
  template: "reset-password-request",
});

Execution of this method will result in sending email with compiled liquid template as html:

<html>
  <head>
    <style></style>
  </head>
  <body>
    <p>Hi John,</p>
    <p>You requested to reset your password.</p>
    <p>Please, click the link below to reset your password</p>

    <a href="http://example.com/create-new-password">Reset Password</a>
  </body>
</html>

Conclussions

In conclusion, LiquidJS is a mature and widely-used template engine that is created by Shopify. Its popularity is due to its standard syntax which is used in Jekyll sites, Github pages, and Shopify templates. Its versatility and ease of use make it a great choice for developers looking to create dynamic templates for various applications.

It is a great choice for small implementations, particularly as an email template engine.

Additional resources