# @fastify/view  [](https://www.npmjs.com/package/@fastify/view) [](https://standardjs.com/) 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 templates responses. Currently supports the following templates engines: - [`ejs`](https://ejs.co/) - [`nunjucks`](https://mozilla.github.io/nunjucks/) - [`pug`](https://pugjs.org/api/getting-started.html) - [`handlebars`](https://handlebarsjs.com/) - [`mustache`](https://mustache.github.io/) - [`art-template`](https://aui.github.io/art-template/) - [`twig`](https://twig.symfony.com/) - [`liquid`](https://github.com/harttle/liquidjs) - [`doT`](https://github.com/olado/doT) - [`eta`](https://eta.js.org) 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](#migrating-from-view-to-viewAsync)._ _Note: [`ejs-mate`](https://github.com/JacksonTian/ejs-mate) support [has been dropped](https://github.com/fastify/point-of-view/pull/157)._ _Note: [`marko`](https://markojs.com/) support has been dropped. Please use [`@marko/fastify`](https://github.com/marko-js/fastify) instead._ #### Benchmarks The benchmark 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: ```html
Hello, <%= name %>!
``` ```js // 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 | Option | Description | Default | | ---------------------- | ----------- | ------- | | `engine` | **Required**. The template engine object - pass in the return value of `require('<%= text %>
``` ```js // 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. ```js 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: ```js 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: ```js // 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](#migrating-from-view-to-viewAsync). ## Registering multiple engines Registering multiple engines with different configurations is supported. They are distinguished via their `propertyName`: ```js 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" }); }); ``` ## Minifying HTML on render To utilize [`html-minifier-terser`](https://www.npmjs.com/package/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: ```js // 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 filter some paths from minification, you can add the option `pathsToExcludeHtmlMinifier` with list of paths ```js // 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: ```js options: { partials: { header: 'header.mustache', footer: 'footer.mustache' } } ``` ```js fastify.get('/', (req, reply) => { reply.view('./templates/index.mustache', data) }) ``` ```js 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) } }) }) ``` ```js 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: ```js options: { partials: { header: 'header.hbs', footer: 'footer.hbs' } } ``` You can specify [compile options](https://handlebarsjs.com/api-reference/compilation.html#handlebars-compile-template-options) as well: ```js options: { compileOptions: { preventIndent: true } } ``` To access `defaultContext` and `reply.locals` as [`@data` variables](https://handlebarsjs.com/api-reference/data-variables.html): ```js options: { useDataVariables: true } ``` To use layouts in handlebars you will need to pass the `layout` parameter: ```js fastify.register(require("@fastify/view"), { engine: { handlebars: require("handlebars"), }, layout: "./templates/layout.hbs", }); fastify.get("/", (req, reply) => { reply.view("./templates/index.hbs", { text: "text" }); }); ``` ```js 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) } }) }) ``` ```js 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: ```js 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: ```js options: { onConfigure: (env) => { // do whatever you want on nunjucks env }; } ``` ```js fastify.get('/', (req, reply) => { reply.view('./templates/index.njk', data) }) ``` ```js 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) } }) }) ``` ```js 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: ```js 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" }); }); ``` ```js 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) } }) }) ``` ```js 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](https://github.com/olado/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](https://github.com/olado/doT#security-considerations). To make it possible it is necessary to provide a `root` or `templates` option with the path to the template directory. ```js 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" }); }); ``` ```js const d = dot.process({ path: 'templates', destination: 'out' }) fastify.get('/', (req, reply) => { reply.view(d.index, data) }) ``` ```js fastify.get('/', (req, reply) => { reply.view({ raw: readFileSync('./templates/index.dot'), imports: { def: readFileSync('./templates/index.def') } }, data) }) ``` ### eta ```js 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" }); }); ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.eta', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view(eta.compile(file), data) } }) }) ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.eta', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view({ raw: file }, data) } }) }) ``` ### ejs ```js const ejs = require('ejs') fastify.register(pointOfView, { engine: { ejs }, templates: 'templates' }) fastify.get("/", (req, reply) => { reply.view("index.ejs", { text: "text" }); }); ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.ejs', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view(ejs.compile(file), data) } }) }) ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.ejs', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view({ raw: file }, data) } }) }) ``` ### pug ```js const pug = require('pug') fastify.register(pointOfView, { engine: { pug } }) fastify.get("/", (req, reply) => { reply.view("index.pug", { text: "text" }); }); ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.pug', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view(pug.compile(file), data) } }) }) ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.pug', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view({ raw: file }, data) } }) }) ``` ### twig ```js const twig = require('twig') fastify.register(pointOfView, { engine: { twig } }) fastify.get("/", (req, reply) => { reply.view("index.twig", { text: "text" }); }); ``` ```js 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) } }) }) ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.twig', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view({ raw: file }, data) } }) }) ``` ### art ```js const art = require('art-template') fastify.register(pointOfView, { engine: { 'art-template': art } }) fastify.get("/", (req, reply) => { reply.view("./index.art", { text: "text" }); }); ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.art', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view(art.compile({ filename: path.join(__dirname, '..', 'templates', 'index.art') }), data) } }) }) ``` ```js fastify.get('/', (req, reply) => { fs.readFile('./templates/index.art', 'utf8', (err, file) => { if (err) { reply.send(err) } else { reply.view({ raw: file }, data) } }) }) ``` ## Miscellaneous ### Using @fastify/view as a dependency in a fastify-plugin To require `@fastify/view` as a dependency to a [fastify-plugin](https://github.com/fastify/fastify-plugin), add the name `@fastify/view` to the dependencies array in the [plugin's opts](https://github.com/fastify/fastify-plugin#dependencies). ```js fastify.register(myViewRendererPlugin, { dependencies: ["@fastify/view"], }); ``` ### Forcing a cache-flush To forcefully clear cache when in production mode, call the `view.clearCache()` function. ```js 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: ```js fastify.get('/', (req, reply) => { reply.view('index.ejs', { text: 'text' }) }) ``` Now: ```js fastify.get('/', (req, reply) => { return reply.viewAsync('index.ejs', { text: 'text' }) }) ``` #### Async handler Previously: ```js // This is an async function fastify.get("/", async (req, reply) => { const data = await something(); reply.view("/templates/index.ejs", { data }); return }) ``` Now: ```js // 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: ```js // Promise based, using async/await fastify.get("/", async (req, reply) => { const html = await fastify.view("/templates/index.ejs", { text: "text" }); return html }) ``` ```js // 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: ```js // Promise based, using async/await fastify.get("/", (req, reply) => { const html = await fastify.viewAsync("/templates/index.ejs", { text: "text" }); return html }) ``` ```js 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`. ## Acknowledgements This project is kindly sponsored by: - [nearForm](https://nearform.com) - [LetzDoIt](https://www.letzdoitapp.com/) ## License Licensed under [MIT](./LICENSE).