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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
// Safe usage:
const title = '<script>alert("xss")</script>';
const html = `<p>${escape(title)}</p>`;
// → <p><script>alert("xss")</script></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()wrapscontent()wrapsitem() - 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.