Template Engines

Difficulty: Beginner

Template engines allow you to generate dynamic HTML on the server by embedding data into HTML templates. Instead of sending raw HTML strings from your route handlers, you create template files with placeholders that get replaced with actual data at render time. This is called server-side rendering (SSR) and was the standard way to build web applications before single-page applications (SPAs) became popular.

Express has built-in support for template engines through its view system. You configure it by setting two properties: 'view engine' tells Express which template engine to use (e.g., 'ejs', 'pug'), and 'views' tells Express where to find template files (defaults to a 'views' directory). Once configured, you use res.render('templateName', data) to render a template with dynamic data and send the resulting HTML to the client.

EJS (Embedded JavaScript) is one of the most popular template engines because it uses plain HTML with embedded JavaScript expressions. The syntax is straightforward: <%= expression %> outputs an HTML-escaped value, <%- expression %> outputs raw unescaped HTML, and <% code %> executes JavaScript without outputting anything (used for loops, conditionals, etc.). EJS templates look like regular HTML files with special tags, making them easy to learn.

Pug (formerly Jade) takes a completely different approach with a whitespace-sensitive, indentation-based syntax that eliminates the need for closing tags, angle brackets, and most quotes. While Pug produces cleaner template code, it has a steeper learning curve and the syntax looks nothing like HTML. The choice between EJS and Pug is largely a team preference.

While many modern applications use client-side rendering with React, Vue, or Angular, template engines are still valuable for certain use cases: server-rendered marketing pages for better SEO, email templates, admin dashboards, server-rendered error pages, and applications where JavaScript is not available on the client.

Code examples

EJS Template Engine Setup

const express = require('express');
const path = require('path');
const app = express();

// Configure EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

// Route that renders a template
app.get('/', (req, res) => {
  res.render('home', {
    title: 'My Website',
    username: 'Alice',
    items: ['Node.js', 'Express', 'EJS']
  });
});

app.listen(3000, () => console.log('Server on port 3000'));

// --- views/home.ejs ---
// <!DOCTYPE html>
// <html>
// <head><title><%= title %></title></head>
// <body>
//   <h1>Welcome, <%= username %>!</h1>
//   <h2>Learning:</h2>
//   <ul>
//     <% items.forEach(item => { %>
//       <li><%= item %></li>
//     <% }) %>
//   </ul>
// </body>
// </html>

app.set('view engine', 'ejs') tells Express to use EJS. res.render('home', data) looks for views/home.ejs and passes the data object. Inside the template, <%= %> outputs values and <% %> runs JavaScript logic like loops.

EJS Partials and Layouts

// --- views/partials/header.ejs ---
// <!DOCTYPE html>
// <html>
// <head>
//   <title><%= title %></title>
//   <link rel="stylesheet" href="/css/style.css">
// </head>
// <body>
//   <nav>
//     <a href="/">Home</a>
//     <a href="/about">About</a>
//   </nav>

// --- views/partials/footer.ejs ---
//   <footer><p>&copy; 2026 My App</p></footer>
// </body>
// </html>

// --- views/home.ejs ---
// <%- include('partials/header') %>
//   <main>
//     <h1><%= title %></h1>
//     <p>Welcome to the home page!</p>
//   </main>
// <%- include('partials/footer') %>

const express = require('express');
const path = require('path');
const app = express();

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));

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

app.get('/about', (req, res) => {
  res.render('about', { title: 'About Us' });
});

app.listen(3000);

EJS partials are reusable template fragments included with <%- include('path') %>. Note the use of <%- (unescaped output) instead of <%= because the partial contains HTML that should not be escaped. This pattern avoids duplicating header/footer HTML across every page.

Pug Template Basics

const express = require('express');
const path = require('path');
const app = express();

// Configure Pug as the view engine
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));

app.get('/', (req, res) => {
  res.render('home', {
    title: 'Pug Example',
    user: { name: 'Bob', role: 'Developer' },
    skills: ['JavaScript', 'Node.js', 'Express']
  });
});

app.listen(3000);

// --- views/home.pug ---
// doctype html
// html
//   head
//     title= title
//   body
//     h1 Welcome, #{user.name}
//     p Role: #{user.role}
//     h2 Skills:
//     ul
//       each skill in skills
//         li= skill

Pug uses indentation instead of closing tags. '= value' outputs an expression. #{variable} interpolates inside text. 'each item in array' creates loops. The syntax is concise but requires learning Pug-specific conventions.

Key points

Concepts covered

EJS, Pug, Server-Side Rendering, res.render, View Engine, Passing Data to Views