Suggest an editImprove this articleRefine the answer for “How to use template engines in Express.js?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Template engines** in Express.js render dynamic HTML on the server by merging a template file with a data object. Install the engine, configure it with `app.set('view engine', 'ejs')`, then call `res.render('view', data)` in your routes. ```js app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.get('/', (req, res) => res.render('home', { title: 'Hello' })); ``` **Key:** use for server-rendered HTML apps; skip for JSON APIs or React/Vue frontends.Shown above the full answer for quick recall.Answer (EN)Image**Template engines** in Express.js generate dynamic HTML on the server by merging a template file with a data object. Instead of writing `res.send('<h1>' + name + '</h1>')`, you keep markup in `.ejs` or `.pug` files and call `res.render('view', data)` from your route handlers. ## Theory ### TL;DR - Template = a file with placeholders; `res.render('view', data)` fills them and sends HTML to the browser - Think of it like Mad Libs: the template is the story with blanks, data fills them at runtime - EJS is the most common starting point because it is just HTML with `<% %>` tags - Use a template engine when you send HTML; skip it for JSON APIs or React/Vue frontends - Express does not bundle any engine, so `npm install ejs` (or pug, handlebars) comes first ### Quick setup ```bash npm install ejs ``` ```js // app.js const express = require('express'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); // absolute path avoids lookup errors app.get('/', (req, res) => { res.render('home', { title: 'Hello', users: ['Alice', 'Bob'] }); }); app.listen(3000); ``` ```html <!-- views/home.ejs --> <h1><%= title %></h1> <ul> <% users.forEach(user => { %> <li><%= user %></li> <% }); %> </ul> <!-- Output: <h1>Hello</h1><ul><li>Alice</li><li>Bob</li></ul> --> ``` Express loads the `.ejs` file, compiles it to a Node.js function, injects the data, and sends back a finished HTML string. The compiled function stays in memory, so repeated requests skip the disk read. ### Popular engines | Engine | File | Style | Best for | |---|---|---|---| | **EJS** | `.ejs` | HTML with `<% %>` tags | Quick starts, HTML-first devs | | **Pug** | `.pug` | Indentation-based, no closing tags | Concise, component-like markup | | **Handlebars** | `.hbs` | Logic-less `{{ }}` | Simple data display | | **Nunjucks** | `.njk` | Jinja2-like syntax | Complex loops and filters | EJS is the most common for juniors because the syntax looks like plain HTML. Pug reduces boilerplate. Handlebars keeps templates clean by discouraging logic inside them. None is objectively best. Pick based on what your team already knows. ### EJS tag reference | Tag | What it does | |---|---| | `<%= value %>` | Outputs the value, HTML-escaped | | `<%- value %>` | Outputs raw HTML, unescaped | | `<% code %>` | Runs JavaScript, no output | | `<%- include('partial') %>` | Injects another template file | The `<%= %>` vs `<%- %>` difference matters for security. User-generated content always goes through `<%= %>` because it escapes `<`, `>`, and `&`. The `<%- %>` tag renders raw HTML, which opens XSS if you pass untrusted data through it. ### When to use - Blog or dashboard with data from a database: template engine fits well - API that returns JSON: skip it, use `res.json()` - SPA with React or Vue on the frontend: skip it, the client handles rendering - Email or HTML report generated on demand: template engine works here too - Quick prototype where you want to see HTML fast: EJS, zero learning curve ### Partials and global variables EJS supports partials through `<%- include('header') %>`. This lets you reuse navigation, footers, and page frames without copying HTML across files. For data that every template needs, like the app name or current year, use `app.locals`: ```js app.locals.siteName = 'MyApp'; app.locals.year = new Date().getFullYear(); // Now <%= siteName %> works in every template without passing it each time ``` For per-request data such as the logged-in user, use `res.locals` in middleware: ```js app.use((req, res, next) => { res.locals.currentUser = req.user || null; next(); }); ``` Most admin panels I have seen use exactly this pattern: `app.locals` for static config, `res.locals` for auth context, and per-render data only for what changes between routes. ### How Express handles rendering `res.render('dashboard', data)` does this: loads `views/dashboard.ejs` from disk on the first request, compiles the template to a JavaScript function, caches that function in memory, calls it with the merged locals (your data plus `app.locals` plus `res.locals`), and sends the result. In `NODE_ENV=production` caching is always on. In development, Express recompiles on each request so you see changes without restarting. ### Common mistakes **Not installing the engine package.** `app.set('view engine', 'ejs')` just tells Express what to look for. If you skip `npm install ejs`, you get `Cannot find module 'ejs'` at runtime. Nothing in Express bundles it for you. **Using a relative path for views.** ```js // Breaks when the process starts from a different directory app.set('views', './views'); // Always use this app.set('views', path.join(__dirname, 'views')); ``` **Using `<%- %>` with user input.** ```html <!-- XSS: renders <script>alert(1)</script> as actual HTML --> <li><%- user.name %></li> <!-- Safe: outputs <script>alert(1)</script> as visible text --> <li><%= user.name %></li> ``` **Forgetting to pass data.** `res.render('users')` with no second argument means the template gets no variables. Accessing `<%= users.length %>` throws `Cannot read properties of undefined`. **Mixing `res.render()` and `res.json()` in one route.** Pick one per endpoint. Calling both causes a `Cannot set headers after they are sent` error. ### Real-world usage - Ghost (blog platform) uses server-rendered templates for post pages - Handlebars powers Shopify themes, rendering storefront product data per request - Mozilla docs site uses Nunjucks for complex filter logic, similar to Python Flask - Express + EJS is a common stack for admin dashboards and bootcamp projects where SEO and fast initial load matter more than interactivity ### Follow-up questions **Q:** How does caching work in production? **A:** In `NODE_ENV=production`, Express caches compiled template functions after the first render. In development it recompiles every time so edits show immediately. You do not configure this manually. **Q:** What is the difference between `<% %>`, `<%= %>`, and `<%- %>` in EJS? **A:** `<% %>` runs JavaScript with no output. `<%= %>` outputs a value with HTML escaping. `<%- %>` outputs raw HTML without escaping. For anything from a user or a database, always use `<%= %>`. **Q:** How does `res.render` differ from `res.send`? **A:** `res.render` compiles a template file with data and sends the result. `res.send` takes whatever string or buffer you pass directly. No template file involved. **Q:** Can two routes render the same template without data leaking between requests? **A:** Yes. Express caches the compiled function but each call executes it with fresh locals. Data from one request never bleeds into another. Anything on `app.locals` is global, so only put truly static values there. **Q:** Pug or EJS for performance under high traffic? **A:** After the first compile both become JavaScript functions and perform about the same. Pug's initial parse is slightly slower but that difference disappears after caching. This is not a meaningful factor when choosing an engine. ## Examples ### Basic EJS page with a user list ```js // app.js const express = require('express'); const path = require('path'); const app = express(); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, 'views')); app.get('/users', (req, res) => { const users = [ { id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, ]; res.render('users', { title: 'User list', users }); }); app.listen(3000); ``` ```html <!-- views/users.ejs --> <h1><%= title %></h1> <ul> <% users.forEach(user => { %> <li><a href="/users/<%= user.id %>"><%= user.name %></a></li> <% }); %> </ul> <% if (users.length === 0) { %> <p>No users found.</p> <% } %> ``` Data from the route flows into the template. The `forEach` renders one `<li>` per user. The empty-state check keeps the page useful when the array is empty. ### Dashboard with partials and auth middleware ```js // app.js app.locals.siteName = 'MyApp'; // available in every template app.use((req, res, next) => { res.locals.currentUser = req.user || null; next(); }); app.get('/dashboard', async (req, res) => { const users = await db.query('SELECT * FROM users LIMIT 10'); res.render('dashboard', { recentUsers: users.rows, stats: { total: users.rows.length }, }); }); ``` ```html <!-- views/partials/header.ejs --> <header> <span><%= siteName %></span> <% if (currentUser) { %> <span>Hello, <%= currentUser.name %></span> <% } %> </header> <!-- views/dashboard.ejs --> <%- include('partials/header') %> <p>Total users: <%= stats.total %></p> <table> <% recentUsers.forEach(u => { %> <tr> <td><%= u.name %></td> <td><%= u.email %></td> </tr> <% }); %> </table> ``` `res.locals.currentUser` set in middleware is available inside the partial without passing it explicitly. `app.locals.siteName` set once at startup appears in every template. This is how real Express apps avoid repetitive data passing across routes. ### Safe vs unsafe output ```js // Route that receives user-generated content app.get('/comment', (req, res) => { const comment = { author: 'Bob', text: '<script>alert("XSS")</script> Nice post!', }; res.render('comment', { comment }); }); ``` ```html <!-- views/comment.ejs --> <strong><%= comment.author %></strong> <!-- Safe output --> <p><%= comment.text %></p> <!-- Browser shows: <script>alert("XSS")</script> Nice post! --> <!-- The angle brackets are escaped, no script runs --> <!-- Dangerous output --> <p><%- comment.text %></p> <!-- Browser executes the script tag --> ``` `<%= %>` is the default choice for every value that comes from a user, a form, or a database. `<%- %>` is for trusted content only, like HTML you construct yourself on the server.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.