Series: Pure Node.js — Zero Dependencies

Post #6: HTML Templating with String Interpolation

📅 March 2026 ⏱ 8 min read 🏷 Node.js, Templates, HTML, XSS

📚 Pure Node.js Series — Zero Dependencies

#1: Build an HTTP Server From Scratch #2: Manual Routing — No Express Needed #3: Serving Static Files (HTML, CSS, JS) #4: Handling Forms & POST Requests #5: JSON Files as a Database → #6: HTML Templating with String Interpolation (you are here) #7: Vanilla JS Charts & Dynamic UI #8: WebSockets From Scratch #9: Chat Rooms & Broadcast Messages #10: Sessions & Cookies Without Packages #11: File Uploads & Image Serving #12: Deploy Pure Node.js to a VPS

Building a Dashboard Without a Template Engine

We start Module 2 — the Dashboard app. A dashboard needs to render dynamic HTML: tables of data, stats, charts. Template engines like EJS or Handlebars make this easy, but they're npm packages. JavaScript's template literals give us everything we need.

The Critical Rule: Always Escape User Data

Before anything else — if you interpolate user data into HTML without escaping, you create an XSS vulnerability. A user could enter <script>alert('hacked')</script> as a todo title and it would execute in every visitor's browser.

// ALWAYS escape user data before putting it in HTML
function escape(str) {
  return String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

// Safe usage:
const title = '<script>alert("xss")</script>';
const html = `<p>${escape(title)}</p>`;
// → <p>&lt;script&gt;alert("xss")&lt;/script&gt;</p>
// Renders as text, not as executable script
Security rule: Use escape() on every piece of user-controlled data you insert into HTML. The only exception is when you explicitly want to insert trusted HTML (e.g., your own template partials).

Building the Layout System

// templates/layout.js
function layout(title, content) {
  return `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>${escape(title)} | Dashboard</title>
  <link rel="stylesheet" href="/style.css">
</head>
<body>
  <nav>
    <a href="/">Dashboard</a>
    <a href="/todos">Todos</a>
    <a href="/stats">Stats</a>
  </nav>
  <main>
    ${content}
  </main>
  <footer><p>Built with pure Node.js</p></footer>
</body>
</html>`;
}

module.exports = { layout, escape };

Rendering Lists

// templates/todos.js
const { layout, escape } = require('./layout');

function todoItem(todo) {
  return `
    <li class="todo-item ${todo.done ? 'done' : ''}">
      <span>${escape(todo.title)}</span>
      <form action="/todos/${escape(todo.id)}/toggle" method="POST">
        <button>${todo.done ? 'Undo' : 'Done'}</button>
      </form>
      <form action="/todos/${escape(todo.id)}/delete" method="POST">
        <button class="danger">Delete</button>
      </form>
    </li>
  `;
}

function todosPage(todos) {
  const items = todos.length
    ? todos.map(todoItem).join('')
    : '<li class="empty">No todos yet. Add one above!</li>';

  const content = `
    <h1>Todos (${todos.length})</h1>
    <form action="/todos" method="POST" class="add-form">
      <input name="title" placeholder="What needs to be done?" required />
      <button type="submit">Add</button>
    </form>
    <ul class="todo-list">${items}</ul>
  `;

  return layout('Todos', content);
}

module.exports = { todosPage };

Rendering Data Tables

function dataTable(headers, rows) {
  const headerRow = headers
    .map(h => `<th>${escape(h)}</th>`)
    .join('');

  const bodyRows = rows
    .map(row =>
      `<tr>${row.map(cell => `<td>${escape(String(cell))}</td>`).join('')}</tr>`
    )
    .join('');

  return `
    <table>
      <thead><tr>${headerRow}</tr></thead>
      <tbody>${bodyRows}</tbody>
    </table>
  `;
}

// Usage:
const table = dataTable(
  ['Title', 'Status', 'Created'],
  todos.map(t => [t.title, t.done ? 'Done' : 'Pending', t.createdAt])
);

Rendering Stats Cards

function statsCard(label, value, unit = '') {
  return `
    <div class="stat-card">
      <div class="stat-value">${escape(String(value))}${unit}</div>
      <div class="stat-label">${escape(label)}</div>
    </div>
  `;
}

function dashboardPage(todos) {
  const total = todos.length;
  const done = todos.filter(t => t.done).length;
  const pending = total - done;
  const pct = total ? Math.round((done / total) * 100) : 0;

  const content = `
    <h1>Dashboard</h1>
    <div class="stats-grid">
      ${statsCard('Total Todos', total)}
      ${statsCard('Completed', done)}
      ${statsCard('Pending', pending)}
      ${statsCard('Completion', pct, '%')}
    </div>
  `;

  return layout('Dashboard', content);
}
Key takeaways:
  • Template literals (`backticks`) are your template engine
  • Always escape() user data before inserting into HTML
  • Build composable functions: layout() wraps content() wraps item()
  • Arrays of strings joined with .join('') = loops in templates
  • This is exactly how EJS/Handlebars work — just without the package overhead

What's Next

In Post #7, we build the frontend — using the Fetch API and the HTML5 Canvas to create a live dashboard with charts, all in vanilla JavaScript with zero libraries.

Building along? Share on X/Twitter or GitHub.