Every earlier lesson in this course either returned JSON (a REST API) or assumed a separate frontend. A template engine is the traditional alternative — the server renders complete HTML pages directly, with data plugged into placeholders.
import express from 'express'
const app = express()
app.set('view engine', 'ejs')
app.set('views', './views') // where template files live<!-- views/profile.ejs -->
<!DOCTYPE html>
<html>
<head><title><%= user.name %>'s Profile</title></head>
<body>
<h1>Welcome, <%= user.name %>!</h1>
<p>Email: <%= user.email %></p>
</body>
</html><%= value %> outputs a value, automatically escaped for safety (the same escaping concept as the earlier Cybersecurity course's XSS-prevention lesson) — plain text stays plain text even if it contains HTML-looking characters.
app.get('/profile/:id', async (req, res) => {
const user = await getUser(req.params.id)
res.render('profile', { user })
})<ul>
<% products.forEach(function(product) { %>
<li><%= product.name %> — $<%= product.price %></li>
<% }) %>
</ul>
<% if (user.isAdmin) { %>
<a href="/admin">Admin Panel</a>
<% } %><% ... %> (no =) runs JavaScript logic without outputting anything directly — loops and conditionals use this form.
<!-- views/partials/header.ejs -->
<header><h1>My Site</h1></header>
<!-- views/profile.ejs -->
<%- include('partials/header') %>
<h2>Profile</h2><%- %> (with a dash) outputs raw, unescaped HTML — needed for include() since a partial's own HTML shouldn't be escaped away.
| Tag | Purpose |
|---|---|
| <%= value %> | Output a value, HTML-escaped |
| <%- html %> | Output raw HTML, not escaped (used for includes) |
| <% code %> | Run JavaScript logic — no output |
One of several template engines
EJS is one option among several (Pug and Handlebars are common alternatives) — the underlying concept (server-rendered HTML with data plugged in) is the same across all of them, this lesson's syntax is EJS-specific.