APIs return JSON, but many sites still render HTML on the server: dashboards, blogs, admin panels, email previews. A template engine lets you write HTML files with embedded placeholders that Express fills with real data at request time. In this lesson we set up EJS, the most beginner-friendly engine, and build dynamic pages with loops, conditionals, and shared partials.
Instead of sending a fixed file, the server combines a template with data and sends the finished HTML. The browser receives a complete page, which is great for SEO and first-load speed. EJS (Embedded JavaScript) is popular because its syntax is just HTML plus small JavaScript tags, so there is almost nothing new to learn.
npm install ejsconst express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));Two settings do all the work: view engine tells Express which engine to use, and views points at the folder containing your templates. With these in place you never require ejs manually; Express loads it on demand.
Create views/home.ejs:
<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
<h1>Welcome, <%= username %>!</h1>
</body>
</html>Then render it from a route, passing data as the second argument:
app.get('/', (req, res) => {
res.render('home', { title: 'My Site', username: 'Aisha' });
});res.render finds views/home.ejs, injects the values, and sends the resulting HTML with the right headers. No file extension needed in the route.
Templates shine when rendering lists. Create views/products.ejs:
<h1>Products</h1>
<% if (products.length === 0) { %>
<p>No products yet.</p>
<% } else { %>
<ul>
<% products.forEach(p => { %>
<li><%= p.name %> - $<%= p.price %></li>
<% }) %>
</ul>
<% } %>app.get('/products', (req, res) => {
const products = [
{ name: 'Keyboard', price: 49 },
{ name: 'Mouse', price: 25 },
];
res.render('products', { products });
});The template stays declarative; the route supplies data. Resist the urge to do heavy logic inside templates, since it quickly becomes untestable.
Headers, footers, and navigation belong in one place. Create views/partials/header.ejs and include it everywhere:
<%- include('partials/header', { active: 'home' }) %>
<main>Page content here</main>
<%- include('partials/footer') %>Note the raw tag <%- for includes, because the partial returns HTML that must not be escaped. Change the header once and every page updates.
Next lesson: Connecting Express to MongoDB, where our data finally survives a server restart.