@fastify/view vs ejs vs express-handlebars vs pug
Server-Side Rendering Templates in Node.js
@fastify/viewejsexpress-handlebarspugSimilar Packages:

Server-Side Rendering Templates in Node.js

@fastify/view, ejs, express-handlebars, and pug are tools used to generate dynamic HTML on the server in Node.js applications. ejs and pug are template engines that define the syntax for mixing logic with HTML. @fastify/view and express-handlebars are framework-specific plugins that integrate these engines into Fastify and Express respectively. While ejs uses standard HTML-like tags, pug relies on indentation, and handlebars emphasizes logic-less templates. Choosing the right tool depends on your framework choice, preferred syntax, and architectural needs for server-side rendering.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
@fastify/view0381344 kB93 months agoMIT
ejs08,127211 kB264 months agoApache-2.0
express-handlebars0282146 kB36 months agoBSD-3-Clause
pug021,84222.5 kB3356 months agoMIT

Server-Side Rendering Templates in Node.js: Architecture and Syntax Compared

When building Node.js applications that serve HTML, you need a way to inject dynamic data into your pages. The packages @fastify/view, ejs, express-handlebars, and pug all solve this problem, but they operate at different layers of the stack. Some are template engines that define the syntax, while others are framework plugins that wire those engines into your server. Let's break down how they differ in architecture, syntax, and integration.

šŸ—ļø Framework Integration: Plugins vs Engines

The most critical architectural decision is understanding which package is an engine and which is a bridge to your framework.

@fastify/view is a plugin specifically for the Fastify framework.

  • It does not parse templates itself; instead, it loads an engine (like EJS or Pug) for you.
  • It decorates the Fastify reply object with a view method.
// @fastify/view: Registering the plugin with an engine
const fastify = require('fastify')();
const view = require('@fastify/view');
const ejs = require('ejs');

fastify.register(view, {
  engine: { ejs: ejs },
  propertyName: 'view'
});

fastify.get('/', (req, reply) => {
  reply.view('index.ejs', { title: 'Fastify App' });
});

express-handlebars is an engine wrapper designed for Express.

  • It configures Handlebars to work seamlessly with Express's res.render.
  • It requires you to set it as the view engine in your Express app configuration.
// express-handlebars: Configuring the engine for Express
const express = require('express');
const exphbs = require('express-handlebars');
const app = express();

app.engine('handlebars', exphbs.create({ defaultLayout: 'main' }).engine);
app.set('view engine', 'handlebars');

app.get('/', (req, res) => {
  res.render('home', { title: 'Express App' });
});

ejs is a standalone template engine.

  • It can work with any framework or run independently.
  • With Express, you simply set it as the view engine without extra wrappers.
// ejs: Direct usage with Express
const express = require('express');
const app = express();

app.set('view engine', 'ejs');

app.get('/', (req, res) => {
  res.render('index', { title: 'EJS App' });
});

pug is also a standalone template engine.

  • Like EJS, it integrates directly with Express via app.set.
  • It compiles templates into JavaScript functions for rendering.
// pug: Direct usage with Express
const express = require('express');
const app = express();

app.set('view engine', 'pug');

app.get('/', (req, res) => {
  res.render('index', { title: 'Pug App' });
});

āœļø Template Syntax: HTML vs Indentation vs Logic-Less

The developer experience changes drastically depending on the syntax you choose.

ejs uses standard HTML with embedded JavaScript tags.

  • You write normal HTML and insert logic using <% %> tags.
  • Easy for frontend developers to read because it looks like regular HTML.
<!-- ejs: template.ejs -->
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
  <% if (showMessage) { %>
    <h1>Hello World</h1>
  <% } %>
</body>
</html>

pug uses indentation to define structure, removing closing tags.

  • You write less code, but the syntax is distinct from standard HTML.
  • Great for rapid prototyping but requires team buy-in on the syntax.
// pug: template.pug
doctype html
html
  head
    title= title
  body
    if showMessage
      h1 Hello World

express-handlebars (Handlebars) uses a logic-less approach with helpers.

  • You cannot write arbitrary JavaScript inside templates; you use helpers like {{#if}}.
  • Enforces cleaner separation between logic and view.
<!-- express-handlebars: views/home.handlebars -->
<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
  {{#if showMessage}}
    <h1>Hello World</h1>
  {{/if}}
</body>
</html>

@fastify/view adopts the syntax of the engine you choose.

  • If you configure it with EJS, you write EJS syntax.
  • If you configure it with Pug, you write Pug syntax.
<!-- @fastify/view: Using EJS engine (same as EJS example) -->
<!-- views/index.ejs -->
<h1><%= title %></h1>

āš™ļø Configuration & Layouts

Managing layouts and partials is a common requirement for professional apps.

express-handlebars has built-in layout support.

  • You define a main.handlebars layout that wraps your content.
  • Configuration is handled during engine creation.
// express-handlebars: Layout configuration
exphbs.create({
  defaultLayout: 'main',
  layoutsDir: 'views/layouts/',
  partialsDir: 'views/partials/'
});

ejs requires manual implementation for layouts.

  • You typically use include to pull in headers and footers.
  • No native layout wrapper, so you repeat structure or use middleware.
<!-- ejs: Manual layout inclusion -->
<%- include('partials/header') %>
<h1>Content</h1>
<%- include('partials/footer') %>

pug uses template inheritance with block and extends.

  • You define a base template and child templates fill in the blocks.
  • Very powerful for nested structures.
// pug: base.pug
html
  body
    block content

// pug: index.pug
extends base.pug
block content
  h1 Hello

@fastify/view relies on the underlying engine for layouts.

  • If using EJS, you manage includes manually.
  • If using Handlebars via the plugin, you get Handlebars layout features.
// @fastify/view: Configuring layouts via engine options
fastify.register(view, {
  engine: { handlebars: require('handlebars') },
  options: {
    layout: 'main',
    layoutsDir: 'views/layouts'
  }
});

🌱 Similarities: Shared Ground

Despite their differences, these tools share core goals and capabilities.

1. šŸ–„ļø Server-Side HTML Generation

  • All four produce HTML strings on the server before sending to the client.
  • Improves SEO and initial load performance compared to client-side rendering.
// Common goal: Rendering a view with data
// All packages ultimately call a render function with data
reply.view('template', { user: 'Alice' }); // Fastify
res.render('template', { user: 'Alice' }); // Express

2. šŸ“¦ Data Passing

  • All accept an object of data to inject into the template.
  • Variables are accessible within the template scope.
// Passing data context
const data = { title: 'Home', user: 'Bob' };
// Used in all engines as 'title' and 'user'

3. šŸ”Œ Extensibility

  • All support custom helpers or functions to extend template logic.
  • You can add formatting functions or utility logic.
// EJS: Custom function in template
<%= formatDate(date) %>

// Handlebars: Register helper
hbs.registerHelper('formatDate', (date) => ...);

šŸ“Š Summary: Key Differences

Feature@fastify/viewexpress-handlebarsejspug
TypeFastify PluginExpress Engine WrapperStandalone EngineStandalone Engine
FrameworkFastifyExpressAny (Commonly Express)Any (Commonly Express)
SyntaxDepends on EngineLogic-less (Handlebars)HTML + JS TagsIndentation-Based
LayoutsVia Engine ConfigBuilt-in SupportManual IncludesInheritance (Extends)
Learning CurveMedium (Fastify + Engine)Medium (Handlebars Rules)Low (HTML-like)Medium (New Syntax)

šŸ’” The Big Picture

@fastify/view is the go-to choice for Fastify users.

  • It is not an engine itself but the bridge that lets Fastify use engines.
  • Ideal if you have chosen Fastify for performance and need SSR capabilities.

express-handlebars is best for Express teams wanting structure.

  • The logic-less approach prevents complex code in views.
  • Great for large teams where separation of concerns is critical.

ejs is the safest bet for simplicity.

  • If your team knows HTML, they know EJS.
  • Perfect for small to medium projects where speed of development matters.

pug is for teams who love concise code.

  • Reduces boilerplate significantly.
  • Best if you are comfortable with whitespace-sensitive syntax and want powerful template features.

Final Thought: Your choice depends first on your framework (Fastify vs Express), and second on your team's syntax preference. If you are on Fastify, @fastify/view is mandatory. If on Express, choose between ejs, pug, or express-handlebars based on how much logic you want in your views.

How to Choose: @fastify/view vs ejs vs express-handlebars vs pug

  • @fastify/view:

    Choose @fastify/view if you are building with the Fastify framework and need a robust plugin to manage view rendering. It supports multiple engines like EJS, Pug, and Handlebars, allowing you to switch templating languages without changing your server setup. It is ideal for high-performance Fastify applications that require tight integration with the framework's lifecycle and decorators.

  • ejs:

    Choose ejs if you prefer a simple, HTML-like syntax that is easy for frontend developers to read and adopt. It is framework-agnostic, meaning you can use it with Express, Fastify, or standalone scripts without heavy configuration. It is best suited for projects where team familiarity with standard HTML tags outweighs the need for advanced template features or indentation-based brevity.

  • express-handlebars:

    Choose express-handlebars if you are using Express and want a strict, logic-less templating approach that enforces separation of concerns. It provides built-in support for layouts and partials, which helps maintain consistency across large applications. It is suitable for teams that prioritize maintainable, component-based views and are already invested in the Express ecosystem.

  • pug:

    Choose pug if you value concise, whitespace-sensitive syntax that reduces boilerplate code significantly. It offers powerful features like mixins and inheritance, making it great for complex UI structures. It is best for teams comfortable with indentation-based languages who want to write less code and leverage advanced template capabilities within Express or other Node.js servers.

README for @fastify/view

@fastify/view

CI NPM version neostandard javascript style

Templates rendering plugin support for Fastify.

@fastify/view decorates the reply interface with the view and viewAsync methods for managing view engines, which can be used to render templated responses.

Currently supports the following templates engines:

In production mode, @fastify/view will heavily cache the templates file and functions, while in development will reload every time the template file and function.

Note: For Fastify v3 support, please use point-of-view 5.x (npm i point-of-view@5).

Note that at least Fastify v2.0.0 is needed.

Recent Changes

Note: reply.viewAsync added as a replacement for reply.view and fastify.view. See Migrating from view to viewAsync.

Note: ejs-mate support has been dropped.

Note: marko support has been dropped. Please use @marko/fastify instead.

Benchmarks

The benchmarks were run with the files in the benchmark folder with the ejs engine. The data has been taken with: autocannon -c 100 -d 5 -p 10 localhost:3000

  • Express: 8.8k req/sec
  • Fastify: 15.6k req/sec

Install

npm i @fastify/view

Quick start

fastify.register is used to register @fastify/view. By default, It will decorate the reply object with a view method that takes at least two arguments:

  • the template to be rendered
  • the data that should be available to the template during rendering

This example will render the template using the EJS engine and provide a variable name to be used inside the template:

<!-- index.ejs --->
<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <p>Hello, <%= name %>!</p>
  </body>
</html>
// index.js:
const fastify = require("fastify")()
const fastifyView = require("@fastify/view")

fastify.register(fastifyView, {
  engine: {
    ejs: require("ejs")
  }
})

// synchronous handler:
fastify.get("/", (req, reply) => {
  reply.view("index.ejs", { name: "User" });
})

// asynchronous handler:
fastify.get("/", async (req, reply) => {
  return reply.viewAsync("index.ejs", { name: "User" });
})

fastify.listen({ port: 3000 }, (err) => {
  if (err) throw err;
  console.log(`server listening on ${fastify.server.address().port}`);
})

Configuration

Options

OptionDescriptionDefault
engineRequired. The template engine object - pass in the return value of require('<engine>')
productionEnables caching of template files and render functionsNODE_ENV === "production"
maxCacheIn production mode, maximum number of cached template files and render functions100
defaultContextTemplate variables available to all views. Variables provided on render have precedence and will override this if they have the same name.

Example: { siteName: "MyAwesomeSite" }
{}
propertyNameThe property that should be used to decorate reply and fastify

E.g. reply.view() and fastify.view() where "view" is the property name
"view"
asyncPropertyNameThe property that should be used to decorate reply for async handler

Defaults to ${propertyName}Async if propertyName is defined
"viewAsync"
rootThe root path of your templates folder. The template name or path passed to the render function will be resolved relative to this path"./"
charsetDefault charset used when setting Content-Type header"utf-8"
includeViewExtensionAutomatically append the default extension for the used template engine if omitted from the template name. So instead of template.hbs, just template can be usedfalse
viewExtOverride the default extension for a given template engine. This has precedence over includeViewExtension and will lead to the same behavior, just with a custom extension.

Example: "handlebars"
""
layoutSee Layouts

This option lets you specify a global layout file to be used when rendering your templates. Settings like root or viewExt apply as for any other template file.

Example: ./templates/layouts/main.hbs
optionsSee Engine-specific settings{}

Example

fastify.register(require("@fastify/view"), {
  engine: {
    handlebars: require("handlebars"),
  },
  root: path.join(__dirname, "views"), // Points to `./views` relative to the current file
  layout: "./templates/template", // Sets the layout to use to `./views/templates/layout.handlebars` relative to the current file.
  viewExt: "handlebars", // Sets the default extension to `.handlebars`
  propertyName: "render", // The template can now be rendered via `reply.render()` and `fastify.render()`
  defaultContext: {
    dev: process.env.NODE_ENV === "development", // Inside your templates, `dev` will be `true` if the expression evaluates to true
  },
  options: {}, // No options passed to handlebars
});

Layouts

@fastify/view supports layouts for EJS, Handlebars, Eta and doT. When a layout is specified, the request template is first rendered, then the layout template is rendered with the request-rendered html set on body.

Example

<!-- layout.ejs: -->
<!DOCTYPE html>
<html lang="en">
  <head></head>
  <body>
    <!--
      Ensure body is not escaped:

      EJS: <%- body %>
      Handlebars: {{{ body }}}
      ETA/doT: <%~ it.body %>
    -->
    <%- body %>
    <br/>
  </body>
</html>
<!-- template.ejs: -->
<p><%= text %></p>
// index.js:
fastify.register(fastifyView, {
  engine: { ejs },
  layout: "layout.ejs"
})

fastify.get('/', (req, reply) => {
  const data = { text: "Hello!"}
  reply.view('template.ejs', data)
})

Providing a layout on render

Please note: Global layouts and providing layouts on render are mutually exclusive. They can not be mixed.

fastify.get('/', (req, reply) => {
  const data = { text: "Hello!"}
  reply.view('template.ejs', data, { layout: 'layout.ejs' })
})

Setting request-global variables

Sometimes, several templates should have access to the same request-specific variables. E.g. when setting the current username.

If you want to provide data, which will be depended on by a request and available in all views, you have to add property locals to reply object, like in the example below:

fastify.addHook("preHandler", function (request, reply, done) {
  reply.locals = {
    text: getTextFromRequest(request), // it will be available in all views
  };

  done();
});

Properties from reply.locals will override those from defaultContext, but not from data parameter provided to reply.view(template, data) function.

Rendering the template into a variable

The fastify object is decorated the same way as reply and allows you to just render a view into a variable (without request-global variables) instead of sending the result back to the browser:

// Promise based, using async/await
const html = await fastify.view("/templates/index.ejs", { text: "text" });

// Callback based
fastify.view("/templates/index.ejs", { text: "text" }, (err, html) => {
  // Handle error
  // Do something with `html`
});

If called within a request hook and you need request-global variables, see Migrating from view to viewAsync.

Registering multiple engines

Registering multiple engines with different configurations is supported. They are distinguished via their propertyName:

fastify.register(require("@fastify/view"), {
  engine: { ejs: ejs },
  layout: "./templates/layout-mobile.ejs",
  propertyName: "mobile",
});

fastify.register(require("@fastify/view"), {
  engine: { ejs: ejs },
  layout: "./templates/layout-desktop.ejs",
  propertyName: "desktop",
});

fastify.get("/mobile", (req, reply) => {
  // Render using the `mobile` render function
  return reply.mobile("/templates/index.ejs", { text: "text" });
});

fastify.get("/desktop", (req, reply) => {
  // Render using the `desktop` render function
  return reply.desktop("/templates/index.ejs", { text: "text" });
});

Rendering a template from a string ("raw" template)

The reply.view({ raw }) option allows you to render a template from a string instead of a file. This is useful when you want to render a template that is not stored in a file, or when you want to use a template that is generated dynamically.

fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.mustache', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

Note that by using the raw option, you are considering the template as trusted - @fastify/view does not perform any validation on the template content.

DO NOT USE raw with untrusted content, or you will make yourself vulnerable to Remote Code Execution (RCE) attacks.

Minifying HTML on render

To utilize html-minifier-terser in the rendering process, you can add the option useHtmlMinifier with a reference to html-minifier-terser, and the optional htmlMinifierOptions option is used to specify the html-minifier-terser options:

// get a reference to html-minifier-terser
const minifier = require('html-minifier-terser')
// optionally defined the html-minifier-terser options
const minifierOpts = {
  removeComments: true,
  removeCommentsFromCDATA: true,
  collapseWhitespace: true,
  collapseBooleanAttributes: true,
  removeAttributeQuotes: true,
  removeEmptyAttributes: true
}
// in template engine options configure the use of html-minifier
  options: {
    useHtmlMinifier: minifier,
    htmlMinifierOptions: minifierOpts
  }

To exclude paths from minification, you can add the option pathsToExcludeHtmlMinifier with a list of paths:

// get a reference to html-minifier-terser
const minifier = require('html-minifier-terser')
// in options configure the use of html-minifier-terser and set paths to exclude from minification
const options = {
  useHtmlMinifier: minifier,
  pathsToExcludeHtmlMinifier: ['/test']
}

fastify.register(require("@fastify/view"), {
  engine: {
    ejs: require('ejs')
  },
  options
});

// This path is excluded from minification
fastify.get("/test", (req, reply) => {
  reply.view("./template/index.ejs", { text: "text" });
});

Engine-specific settings

Mustache

To use partials in mustache you will need to pass the names and paths in the options parameter:

  options: {
    partials: {
      header: 'header.mustache',
      footer: 'footer.mustache'
    }
  }
fastify.get('/', (req, reply) => {
  reply.view('./templates/index.mustache', data)
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.mustache', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      const render = mustache.render.bind(mustache, file)
      reply.view(render, data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.mustache', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

Handlebars

To use partials in handlebars you will need to pass the names and paths in the options parameter:

  options: {
    partials: {
      header: 'header.hbs',
      footer: 'footer.hbs'
    }
  }

You can specify compile options as well:

  options: {
    compileOptions: {
      preventIndent: true
    }
  }

To access defaultContext and reply.locals as @data variables:

  options: {
    useDataVariables: true
  }

To use layouts in handlebars you will need to pass the layout parameter:

fastify.register(require("@fastify/view"), {
  engine: {
    handlebars: require("handlebars"),
  },
  layout: "./templates/layout.hbs",
});

fastify.get("/", (req, reply) => {
  reply.view("./templates/index.hbs", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.hbs', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      const render = handlebars.compile(file)
      reply.view(render, data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.hbs', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

Nunjucks

You can load templates from multiple paths when using the nunjucks engine:

fastify.register(require("@fastify/view"), {
  engine: {
    nunjucks: require("nunjucks"),
  },
  templates: [
    "node_modules/shared-components",
    "views",
  ],
});

To configure nunjucks environment after initialization, you can pass callback function to options:

options: {
  onConfigure: (env) => {
    // do whatever you want on nunjucks env
  };
}
fastify.get('/', (req, reply) => {
  reply.view('./templates/index.njk', data)
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.njk', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      const render = nunjucks.compile(file)
      reply.view(render, data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.njk', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

Liquid

To configure liquid you need to pass the engine instance as engine option:

const { Liquid } = require("liquidjs");
const path = require('node:path');

const engine = new Liquid({
  root: path.join(__dirname, "templates"),
  extname: ".liquid",
});

fastify.register(require("@fastify/view"), {
  engine: {
    liquid: engine,
  },
});

fastify.get("/", (req, reply) => {
  reply.view("./templates/index.liquid", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.liquid', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      const render = engine.renderFile.bind(engine, './templates/index.liquid')
      reply.view(render, data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.liquid', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

doT

When using doT the plugin compiles all templates when the application starts, this way all .def files are loaded and both .jst and .dot files are loaded as in-memory functions. This behavior is recommended by the doT team here. To make it possible it is necessary to provide a root or templates option with the path to the template directory.

fastify.register(require("@fastify/view"), {
  engine: {
    dot: require("dot"),
  },
  root: "templates",
  options: {
    destination: "dot-compiled", // path where compiled .jst files are placed (default = 'out')
  },
});

fastify.get("/", (req, reply) => {
  // this works both for .jst and .dot files
  reply.view("index", { text: "text" });
});
const d = dot.process({ path: 'templates', destination: 'out' })
fastify.get('/', (req, reply) => {
  reply.view(d.index, data)
})
fastify.get('/', (req, reply) => {
  reply.view({ raw: readFileSync('./templates/index.dot'), imports: { def: readFileSync('./templates/index.def') } }, data)
})

eta

const { Eta } = require('eta')
let eta = new Eta()
fastify.register(pointOfView, {
  engine: {
    eta
  },
  templates: 'templates'
})

fastify.get("/", (req, reply) => {
  reply.view("index.eta", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.eta', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view(eta.compile(file), data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.eta', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

ejs

const ejs = require('ejs')
fastify.register(pointOfView, {
  engine: {
    ejs
  },
  templates: 'templates'
})

fastify.get("/", (req, reply) => {
  reply.view("index.ejs", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.ejs', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view(ejs.compile(file), data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.ejs', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

pug

const pug = require('pug')
fastify.register(pointOfView, {
  engine: {
    pug
  }
})


fastify.get("/", (req, reply) => {
  reply.view("index.pug", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.pug', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view(pug.compile(file), data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.pug', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

twig

const twig = require('twig')
fastify.register(pointOfView, {
  engine: {
    twig
  }
})


fastify.get("/", (req, reply) => {
  reply.view("index.twig", { text: "text" });
});
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.twig', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view(twig.twig({ data: file }), data)
    }
  })
})
fastify.get('/', (req, reply) => {
  fs.readFile('./templates/index.twig', 'utf8', (err, file) => {
    if (err) {
      reply.send(err)
    } else {
      reply.view({ raw: file }, data)
    }
  })
})

Edge

const { Edge } = require('edge.js')
const { join } = require('node:path')

const engine = new Edge()
engine.mount(join(__dirname, '..', 'templates'))

fastify.register(require('../index'), {
    engine: {
        edge: engine
    }
})

fastify.get('/', (_req, reply) => {
    reply.view('index.edge', data)
})

Squirrelly

const Sqrl = require('squirrelly')

fastify.register(require('@fastify/view'), {
    engine: {
        squirrelly: Sqrl
    },
    templates: 'templates'
})

fastify.get('/', (_req, reply) => {
    reply.view('index.squirrelly', { text: 'Hello World!' })
})

Miscellaneous

Using @fastify/view as a dependency in a fastify-plugin

To require @fastify/view as a dependency to a fastify-plugin, add the name @fastify/view to the dependencies array in the plugin's opts.

fastify.register(myViewRendererPlugin, {
  dependencies: ["@fastify/view"],
});

Forcing a cache-flush

To forcefully clear the cache when in production mode, call the view.clearCache() function.

fastify.view.clearCache();

Migrating from view to viewAsync

The behavior of reply.view is to immediately send the HTML response as soon as rendering is completed, or immediately send a 500 response with error if encountered, short-circuiting fastify's error handling hooks, whereas reply.viewAsync returns a promise that either resolves to the rendered HTML, or rejects on any errors. fastify.view has no mechanism for providing request-global variables, if needed. reply.viewAsync can be used in both sync and async handlers.

Sync handler

Previously:

fastify.get('/', (req, reply) => {
  reply.view('index.ejs', { text: 'text' })
})

Now:

fastify.get('/', (req, reply) => {
  return reply.viewAsync('index.ejs', { text: 'text' })
})

Async handler

Previously:

// This is an async function
fastify.get("/", async (req, reply) => {
  const data = await something();
  reply.view("/templates/index.ejs", { data });
  return
})

Now:

// This is an async function
fastify.get("/", async (req, reply) => {
  const data = await something();
  return reply.viewAsync("/templates/index.ejs", { data });
})

fastify.view (when called inside a route hook)

Previously:

// Promise based, using async/await
fastify.get("/", async (req, reply) => {
  const html = await fastify.view("/templates/index.ejs", { text: "text" });
  return html
})
// Callback based
fastify.get("/", (req, reply) => {
  fastify.view("/templates/index.ejs", { text: "text" }, (err, html) => {
    if(err) {
      reply.send(err)
    }
    else {
      reply.type("application/html").send(html)
    }
  });
})

Now:

// Promise based, using async/await
fastify.get("/", (req, reply) => {
  const html = await fastify.viewAsync("/templates/index.ejs", { text: "text" });
  return html
})
fastify.get("/", (req, reply) => {
  fastify.viewAsync("/templates/index.ejs", { text: "text" })
    .then((html) => reply.type("application/html").send(html))
    .catch((err) => reply.send(err))
  });
})

Note

By default, views are served with the mime type text/html, with the charset specified in options. You can specify a different Content-Type header using reply.type.

Acknowledgments

This project is kindly sponsored by:

License

Licensed under MIT.