Template Engines & Server-Side Rendering

📖 Concept

While modern apps often use React/Vue for the frontend, server-side rendering (SSR) with template engines is still valuable for:

  • Content-heavy sites (blogs, docs) that need good SEO
  • Admin panels and internal tools
  • Email templates
  • Landing pages that need fast initial load

Popular template engines for Express:

Engine Syntax Pros
EJS <%= variable %> HTML-like, easy to learn
Pug (Jade) Indentation-based Clean, concise
Handlebars {{ variable }} Logic-less, precompilable
Nunjucks {{ variable }} Jinja2-like, powerful

Setting up a template engine:

app.set('view engine', 'ejs');    // Set the engine
app.set('views', './views');       // Set the views directory
app.get('/', (req, res) => {
  res.render('index', { title: 'Home', user: req.user });
});

When to use SSR vs. SPA:

Factor SSR (Template Engine) SPA (React/Vue)
SEO ✅ Excellent Needs extra setup (Next.js)
Initial load ✅ Fast Slower (JS bundle download)
Interactivity Limited ✅ Rich
Complexity Low Higher
Server load Higher (renders each request) Lower (serves static files)

🏠 Real-world analogy: SSR is like a chef plating food in the kitchen before serving — the customer sees the finished dish immediately. SPA is like a build-your-own salad bar — more flexibility but the customer does the work (browser renders the UI).

💻 Code Example

codeTap to expand ⛶
1// Express with EJS Template Engine
2
3const express = require("express");
4const path = require("path");
5const app = express();
6
7// 1. Configure template engine
8app.set("view engine", "ejs");
9app.set("views", path.join(__dirname, "views"));
10
11// Static files
12app.use(express.static("public"));
13app.use(express.urlencoded({ extended: true }));
14
15// Mock data
16const posts = [
17 { id: 1, title: "Getting Started with Node.js", author: "Alice", date: "2024-01-15", excerpt: "Learn the fundamentals..." },
18 { id: 2, title: "Express.js Best Practices", author: "Bob", date: "2024-02-20", excerpt: "Production patterns..." },
19 { id: 3, title: "Database Integration Guide", author: "Charlie", date: "2024-03-10", excerpt: "MongoDB and PostgreSQL..." },
20];
21
22// 2. Routes with template rendering
23app.get("/", (req, res) => {
24 res.render("index", {
25 title: "My Blog",
26 posts,
27 user: req.user || null,
28 });
29});
30
31app.get("/posts/:id", (req, res) => {
32 const post = posts.find((p) => p.id === parseInt(req.params.id));
33 if (!post) {
34 return res.status(404).render("error", {
35 title: "Not Found",
36 message: "Post not found",
37 });
38 }
39 res.render("post", { title: post.title, post });
40});
41
42// 3. EJS template examples:
43
44// views/layout.ejs (partial — header/footer)
45const layoutHeader = `
46<!DOCTYPE html>
47<html lang="en">
48<head>
49 <meta charset="UTF-8">
50 <meta name="viewport" content="width=device-width, initial-scale=1.0">
51 <title><%= title %> | My Blog</title>
52 <link rel="stylesheet" href="/css/style.css">
53</head>
54<body>
55 <nav>
56 <a href="/">Home</a>
57 <% if (typeof user !== 'undefined' && user) { %>
58 <span>Welcome, <%= user.name %></span>
59 <% } else { %>
60 <a href="/login">Login</a>
61 <% } %>
62 </nav>
63 <main>
64`;
65
66// views/index.ejs
67const indexTemplate = `
68<%- include('partials/header', { title, user }) %>
69
70<h1><%= title %></h1>
71
72<% if (posts.length === 0) { %>
73 <p>No posts yet.</p>
74<% } else { %>
75 <% posts.forEach(post => { %>
76 <article>
77 <h2><a href="/posts/<%= post.id %>"><%= post.title %></a></h2>
78 <p class="meta">By <%= post.author %> on <%= post.date %></p>
79 <p><%= post.excerpt %></p>
80 </article>
81 <% }) %>
82<% } %>
83
84<%- include('partials/footer') %>
85`;
86
87// 4. API + SSR hybrid pattern
88// For search engines: render HTML
89// For API clients: return JSON
90app.get("/api/posts", (req, res) => {
91 const acceptsJSON = req.accepts("json");
92 const acceptsHTML = req.accepts("html");
93
94 if (acceptsJSON && !acceptsHTML) {
95 // API client
96 res.json({ data: posts });
97 } else {
98 // Browser
99 res.render("index", { title: "Posts", posts, user: null });
100 }
101});
102
103app.listen(3000, () => {
104 console.log("Blog server at http://localhost:3000");
105});

🏋️ Practice Exercise

Exercises:

  1. Set up EJS as the template engine and create a layout with header, footer, and navigation partials
  2. Build a blog with index page (list posts) and detail page (single post) using template rendering
  3. Create a form that submits data via POST and renders the result on a success page
  4. Implement content negotiation — return JSON for API clients and HTML for browsers
  5. Add flash messages for success/error notifications across page redirects
  6. Build an admin dashboard with a table of data, edit forms, and delete confirmations — all server-rendered

⚠️ Common Mistakes

  • Using <%= %> for HTML content — this escapes HTML entities; use <%- %> for raw HTML (but be careful of XSS!)

  • Not passing all required variables to res.render() — EJS throws ReferenceError if a variable used in the template isn't provided

  • Mixing business logic into templates — templates should only handle display; keep data fetching and processing in controllers

  • Not escaping user input in templates — always use <%= %> (escaped) for user-provided data to prevent XSS attacks

  • Using SSR for everything — highly interactive UIs are better served by SPAs; use SSR for content-heavy, SEO-important pages

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Template Engines & Server-Side Rendering. Login to unlock this feature.