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
1// Express with EJS Template Engine23const express = require("express");4const path = require("path");5const app = express();67// 1. Configure template engine8app.set("view engine", "ejs");9app.set("views", path.join(__dirname, "views"));1011// Static files12app.use(express.static("public"));13app.use(express.urlencoded({ extended: true }));1415// Mock data16const 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];2122// 2. Routes with template rendering23app.get("/", (req, res) => {24 res.render("index", {25 title: "My Blog",26 posts,27 user: req.user || null,28 });29});3031app.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});4142// 3. EJS template examples:4344// 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`;6566// views/index.ejs67const indexTemplate = `68<%- include('partials/header', { title, user }) %>6970<h1><%= title %></h1>7172<% 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<% } %>8384<%- include('partials/footer') %>85`;8687// 4. API + SSR hybrid pattern88// For search engines: render HTML89// For API clients: return JSON90app.get("/api/posts", (req, res) => {91 const acceptsJSON = req.accepts("json");92 const acceptsHTML = req.accepts("html");9394 if (acceptsJSON && !acceptsHTML) {95 // API client96 res.json({ data: posts });97 } else {98 // Browser99 res.render("index", { title: "Posts", posts, user: null });100 }101});102103app.listen(3000, () => {104 console.log("Blog server at http://localhost:3000");105});
🏋️ Practice Exercise
Exercises:
- Set up EJS as the template engine and create a layout with header, footer, and navigation partials
- Build a blog with index page (list posts) and detail page (single post) using template rendering
- Create a form that submits data via POST and renders the result on a success page
- Implement content negotiation — return JSON for API clients and HTML for browsers
- Add flash messages for success/error notifications across page redirects
- 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 providedMixing 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 attacksUsing 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.