Node.js: Template Engines and SSR

Last updated: 2026-08-26

1. Story: Charlie's Management Tool Interface

Charlie is a backend developer at a startup. The team needed an internal management tool to view orders, user statistics, and system status. His colleagues suggested using React or Vue, but Charlie felt those frameworks were too heavy for an internal tool like this—with just a few pages and simple interactions, server-side rendering with a template engine would suffice. He chose EJS because it’s essentially just HTML with a few tags, making it incredibly easy to get started. Within a day, he had built a complete admin interface with reusable layouts using Express and EJS.

You'll learn:



2. Template Rendering Process

The core function of a template engine is to merge template files with data to generate the final HTML string, which is then returned to the browser.

100%
flowchart LR
    A[Browser Request] --> B[Express Routing]
    B --> C[Retrieve Data]
    C --> D["res.render()"]
    D --> E[EJS / Pug Compilation]
    E --> F[Generate HTML]
    F --> G[HTTP Response]

The entire process can be summarized as follows:

  1. The browser sends an HTTP request
  2. Express routes are matched to their corresponding handler functions
  3. The handler retrieves data from a database or API
  4. Call res.render() and pass in the template name and data
  5. The template engine compiles the template and data into HTML
  6. HTML is returned to the browser as an HTTP response


3. EJS Basic Syntax

EJS (Embedded JavaScript) is the most intuitive template engine—templates are simply plain HTML, with logic embedded using <% %> tags.

▶ Example: The Four Core EJS Tags

JAVASCRIPT
// <%= %> Output the escaped value (Safe, prevents XSS)
<p>Hello, <%= userName %>!</p>

// <% %> Execution Logic,No output
<% if (isAdmin) { %>
  <span>Admin Panel</span>
<% } %>

// <%- %> Output Raw HTML (Not escaped, use with caution)
<%- articleHtml %>

// <%- include %> Importing Subtemplates
<%- include('partials/header') %>
▶ Try it Yourself

(1) EJS Syntax Quick Reference

Tag Function Output Escape Example
<%= %> Output variable value Yes Yes <%= name %>
<%- %> Output original content Yes No <%- html %>
<% %> Execution Logic No <% if (x) { %>
<%# %> Comment No <%# comment %>
<%- include('x') %> Include sub-templates Yes No <%- include('nav') %>

▶ Example: EJS Loops and Conditions

JAVASCRIPT
<ul>
  <% items.forEach(function(item) { %>
    <li>
      <strong><%= item.name %></strong>
      <% if (item.onSale) { %>
        <span class="badge">Sale</span>
      <% } %>
    </li>
  <% }); %>
</ul>
▶ Try it Yourself

4. Express + EJS Configuration

▶ Example: Setting Up a Complete Express + EJS Project

Project Structure:

TEXT 📖 Display only
views/
  partials/
    header.ejs
    footer.ejs
  layout.ejs
  index.ejs
app.js
BASH
npm init -y
npm install express ejs
JAVASCRIPT
const express = require('express');
const app = express();

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

app.get('/', (req, res) => {
  res.render('index', {
    title: 'Dashboard',
    user: { name: 'Charlie', role: 'admin' },
    stats: { orders: 128, users: 56 }
  });
});

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

(1) Key API Documentation

API Purpose Example
app.set('view engine', 'ejs') Set the default template engine No need to include the file extension when rendering
app.set('views', path) Set template directory Default is ./views
res.render(view, data) Render the template and respond res.render('index', {title: 'Hi'})
res.render(view, data, callback) Retrieve the HTML string after rendering Can be used for sending emails and other scenarios

▶ Example: Passing data as the second parameter to res.render

JAVASCRIPT
app.get('/profile/:id', (req, res) => {
  const user = { id: req.params.id, name: 'Alice', bio: 'Full-stack dev' };
  res.render('profile', {
    user: user,
    pageTitle: user.name + "'s Profile",
    isLoggedIn: true
  });
});
▶ Try it Yourself

Accessing variables directly by name in the template:

JAVASCRIPT
<h1><%= pageTitle %></h1>
<p>Name: <%= user.name %></p>
<p>Bio: <%= user.bio %></p>


5. Pug Basic Syntax

Pug (formerly known as Jade) uses indentation instead of closing tags, making its syntax extremely concise, but it has a steeper learning curve than EJS.

▶ Example: Basic Pug Syntax

TEXT 📖 Display only
//- Pug Template
doctype html
html
  head
    title= pageTitle
  body
    h1= message
    p Welcome to #{siteName}

Compiled HTML:

TEXT 📖 Display only
<!DOCTYPE html>
<html>
  <head><title>My Page</title></head>
  <body>
    <h1>Hello</h1>
    <p>Welcome to MySite</p>
  </body>
</html>

(1) Pug Syntax Quick Reference

Syntax Function Example
tag= value Property Binding h1= title
#{expr} Interpolation p Hello #{name}
- code Execute JS - const x = 1
if/else if/else Condition if (admin) ...
each val in arr loop each item in list
extends layout Inherit Layout extends layout
block name Definition/Fill Block block content
include path Import sub-templates include header

▶ Example: Pug Loops and Conditions

TEXT 📖 Display only
ul
  each item in items
    li
      strong= item.name
      if item.onSale
        span.badge Sale


6. Reusing Partial Views and Layouts

The core value of a template engine is to avoid having to repeat the header, footer, and HTML skeleton on every page.

▶ Example: EJS Layout + Partial View

views/partials/header.ejs

JAVASCRIPT
<header>
  <nav>
    <a href="/">Home</a>
    <a href="/about">About</a>
  </nav>
</header>
▶ Try it Yourself

views/partials/footer.ejs

JAVASCRIPT
<footer>&copy; 2026 My App</footer>

views/layout.ejs

JAVASCRIPT
<!DOCTYPE html>
<html>
<head>
  <title><%= title %></title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <%- include('partials/header') %>
  <main>
    <%- body %>
  </main>
  <%- include('partials/footer') %>
</body>
</html>

EJS does not have a native layout mechanism; it must be implemented using the express-ejs-layouts middleware or by manually including files.

BASH
npm install express-ejs-layouts
JAVASCRIPT
const expressLayouts = require('express-ejs-layouts');
app.use(expressLayouts);
app.set('layout', 'layout');

▶ Example: Pug Layout Inheritance

views/layout.pug

TEXT 📖 Display only
doctype html
html
  head
    title= title
    link(rel="stylesheet" href="/style.css")
  body
    include partials/header
    block content
    include partials/footer

views/index.pug

TEXT 📖 Display only
extends layout

block content
  h1= pageTitle
  p Welcome!

(1) Comparison of Layout Strategies

Property EJS Pug
Layout Inheritance Requires middleware express-ejs-layouts Native extends + block
Local Import <%- include('partial') %> include partial
Passing Parameters to Child Templates <%- include('x', {data}) %> Passing Data Through the Parent
Multi-block support Limited Native support for multiple blocks


7. Comparison of Template Engines

(1) EJS vs Pug vs Handlebars

Feature EJS Pug Handlebars
Syntax Style HTML + JS Tags Indented, no closing tags Mustache {{}}
Learning Curve Lowest Medium Low
Logical Reasoning Full JS Full JS Restricted (No arbitrary JS)
Architecture/Inheritance Requires middleware Native support Requires additional configuration
Output Escape <%= %> = or #{} {{}}
Original output <%- %> !{} {{{}}}
Use Cases Quick Start / Internal Tools Simplicity First / Full-Stack Projects Security First / Multi-Device Rendering
Community Activity High High Medium

  1. 8 SSR vs CSR Comparison

(1) Key Differences Table

Dimension SSR (Server-Side Rendering) CSR (Client-Side Rendering)
Rendering Location Server Browser
First-screen load speed Fast (HTML loads immediately) Slow (must wait for JS to load and execute)
SEO Friendliness High Low (Content may not be visible to crawlers)
Interaction Complexity Low (page refreshes with each request) High (SPA, no page refresh)
Server Load High Low
Typical Technologies EJS / Pug / PHP React / Vue / Angular
Use Cases Internal Tools / Blogs / SEO Pages Complex Interactions / SPA Applications

▶ Example: Differences in Responses Between SSR and CSR

SSR Response—The browser receives the complete HTML directly:

TEXT 📖 Display only
GET /about
→ Server-Side Rendering HTML Return in Full
→ Display directly in the browser,No need to wait JS

CSR Response—The browser receives an empty shell + JS bundle:

TEXT 📖 Display only
GET /about
→ Server Response <div id="app"></div> + bundle.js
→ Browser Download JS,Execute Rendering
→ Users view the content(Duration of the white screen)


8. Comprehensive Example: EJS Blog Page

Build a complete Express + EJS blog application, including reusable layouts, a post list, post details, and local navigation.

▶ Example: (1) Project Structure

TEXT 📖 Display only
blog/
  views/
    partials/
      header.ejs
      footer.ejs
      nav.ejs
    index.ejs
    post.ejs
  app.js

▶ Example: Application entry point app.js

JAVASCRIPT
const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const app = express();

app.set('view engine', 'ejs');
app.set('views', './views');
app.use(expressLayouts);
app.set('layout', 'layout');

const posts = [
  { id: 1, title: 'Getting Started with Node.js', author: 'Charlie', date: '2026-06-20', excerpt: 'Learn the basics of Node.js runtime.' },
  { id: 2, title: 'Understanding Express Middleware', author: 'Alice', date: '2026-06-25', excerpt: 'Deep dive into middleware patterns.' },
  { id: 3, title: 'Template Engines Compared', author: 'Bob', date: '2026-07-01', excerpt: 'EJS vs Pug vs Handlebars showdown.' }
];

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

app.get('/post/:id', (req, res) => {
  const post = posts.find(p => p.id === parseInt(req.params.id));
  if (!post) return res.status(404).send('Post not found');
  res.render('post', { title: post.title, post: post });
});

app.listen(3000, () => {
  console.log('Blog running on http://localhost:3000');
});
▶ Try it Yourself

▶ Example: Local Navigation partials/nav.ejs

JAVASCRIPT
<nav class="blog-nav">
  <a href="/">All Posts</a>
  <% posts.forEach(function(p) { %>
    <a href="/post/<%= p.id %>"><%= p.title %></a>
  <% }); %>
</nav>
▶ Try it Yourself

▶ Example: Local header partials/header.ejs

JAVASCRIPT
<header class="site-header">
  <h1><%= title %></h1>
</header>
▶ Try it Yourself
JAVASCRIPT
<footer class="site-footer">
  <p>&copy; 2026 My Blog. Powered by Express + EJS.</p>
</footer>
▶ Try it Yourself

▶ Example: Article List Page views/index.ejs

JAVASCRIPT
<h2>Latest Posts</h2>
<div class="post-list">
  <% posts.forEach(function(post) { %>
    <article class="post-card">
      <h3><a href="/post/<%= post.id %>"><%= post.title %></a></h3>
      <p class="meta">By <%= post.author %> on <%= post.date %></p>
      <p><%= post.excerpt %></p>
    </article>
  <% }); %>
</div>
▶ Try it Yourself

▶ Example: Post Details Page views/post.ejs

JAVASCRIPT
<h2><%= post.title %></h2>
<p class="meta">By <%= post.author %> on <%= post.date %></p>
<div class="post-body">
  <p><%= post.excerpt %></p>
  <p>Full article content goes here...</p>
</div>
<a href="/">&larr; Back to all posts</a>
▶ Try it Yourself

How it looks after launching the app:

TEXT 📖 Display only
http://localhost:3000       → Article List Page
http://localhost:3000/post/1 → Article Details Page
http://localhost:3000/post/2 → Second Article

❓ FAQ

Q Is SSR still necessary today?
A SSR is still ideal for internal management tools, pages with high SEO requirements (such as blogs and e-commerce product pages), and simple, content-first sites—without the added complexity of front-end frameworks.
Q Which is better, EJS or Pug?
A EJS is closer to native HTML, has a low learning curve, and is suitable for rapid development; Pug has a more concise syntax but requires getting used to its indentation rules, making it suitable for teams that prioritize code conciseness. EJS is recommended for beginners.
Q How can I reuse layouts in templates?
A EJS uses the express-ejs-layouts middleware to implement layout inheritance, or you can manually import partial views using <%- include() %>; Pug natively supports extends + block to implement layout inheritance.
Q What is the second parameter of res.render?
A It is a data object whose properties can be used directly as variables in the template, such as res.render('index', {title: 'Hi'}); in the template, you would write <%= title %>.
Q Does the template engine support hot reloading?
A During development, you can use nodemon to monitor file changes and automatically restart the service. You can also use the livereload middleware to enable automatic browser refresh, eliminating the need to manually refresh the page.
Q What is the difference between <%- and <%=?
A <%= Outputs HTML-escaped values to prevent XSS attacks; this is the secure option. <%- Outputs raw, unescaped content and should only be used when the content is trusted or when embedding sub-templates.
Q Can I use async/await in templates?
A Top-level await is not supported in EJS templates. You should complete all asynchronous data retrieval in the route and then pass the results to the template using res.render().

📖 Summary


📝 Exercises

  1. Complete all the code examples in this lesson and make sure each one runs correctly.
  2. Modify the comprehensive example and add your own extensions
  3. Review the official documentation, identify 1–2 APIs not covered in this lesson, and write test code for them.
  4. Reflection: How would you apply what you’ve learned in this lesson to a real-world project?
  5. Try to combine what you’ve learned in this lesson with material from previous lessons to build a small project.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏