Static Files & Template Engines
Serve CSS, JS, and images with express.static — render dynamic HTML with EJS, Handlebars, and Pug
express.static
EJS
Handlebars
Pug
1. Serving Static Files in Express
Static files are assets that don't change per request — CSS stylesheets, JavaScript files, images, and favicons. Express provides a built-in middleware function called express.static to serve these files efficiently.
1.1 Basic Static File Serving
To serve files from a directory named public, use:
JavaScriptconst express = require('express');
const app = express();
// Serve all files from the 'public' directory
app.use(express.static('public'));
app.listen(3000);
With this setup:
public/style.css → accessible at http://localhost:3000/style.css
public/logo.png → accessible at http://localhost:3000/logo.png
public/js/app.js → accessible at http://localhost:3000/js/app.js
Express automatically sets the correct Content-Type headers (MIME types) based on file extensions.
1.2 Virtual Path Prefixes
You can mount static serving under a virtual path prefix:
JavaScript// Files in 'public' are served under '/static'
app.use('/static', express.static('public'));
Now:
public/style.css → http://localhost:3000/static/style.css
public/logo.png → http://localhost:3000/static/logo.png
1.3 Multiple Static Directories
You can serve files from multiple directories. Express checks directories in the order they're registered:
JavaScriptapp.use(express.static('public'));
app.use(express.static('uploads'));
If a file exists in both directories, the first match wins.
Best Practice: Use absolute paths to avoid issues when running your app from different directories:
JavaScriptconst path = require('path');
app.use(express.static(path.join(__dirname, 'public')));
1.4 Advanced Options (Caching, Headers)
You can pass options to express.static for finer control:
JavaScriptconst options = {
maxAge: '1d', // Cache for 1 day
etag: true, // Enable ETag generation
lastModified: true, // Add Last-Modified header
setHeaders: (res, filePath) => {
// Custom headers for specific file types
if (path.extname(filePath) === '.html') {
res.setHeader('Cache-Control', 'no-cache');
}
}
};
app.use('/static', express.static('public', options));
Common options:
- maxAge — Cache duration (e.g.,
'1d', '1h', 86400000 in ms)
- index — Default file for directories (default:
'index.html')
- dotfiles — How to handle dotfiles (
'allow', 'deny', 'ignore')
1.5 Serving Single Page Applications (SPA)
For SPAs like React, Vue, or Angular, you need to serve static assets and fall back to index.html for client-side routing:
JavaScriptapp.use(express.static('dist')); // Build folder
// SPA fallback: all unmatched routes serve index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
2. Template Engines Overview
Template engines allow you to generate HTML dynamically by injecting data into templates. Express supports many template engines, including EJS, Handlebars, and Pug.
Basic setup pattern (same for all engines):
JavaScript// 1. Set the views directory (where templates live)
app.set('views', path.join(__dirname, 'views'));
// 2. Set the view engine
app.set('view engine', 'ejs'); // or 'handlebars' or 'pug'
// 3. Render a template in a route
app.get('/', (req, res) => {
res.render('templateName', { data: 'value' });
});
3. EJS (Embedded JavaScript)
EJS is a simple templating language that lets you embed JavaScript logic directly in HTML. Its syntax is very close to writing standard HTML with special tags.
3.1 Installation and Setup
Bashnpm install ejs
JavaScriptconst express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
3.2 EJS Syntax Basics
EJS uses three main tag types:
| Tag | Output | Use Case |
<%= variable %> | Escaped HTML | Display user-generated content (safe) |
<%- htmlContent %> | Raw HTML | Embed HTML strings (be cautious with user input) |
<% code %> | No output | Run JavaScript (loops, conditionals) |
Example template (views/welcome.ejs):
HTML — welcome.ejs<!DOCTYPE html>
<html>
<head>
<title><%= pageTitle %></title>
</head>
<body>
<h1>Welcome, <%= username %>!</h1>
<!-- Raw HTML output -->
<div><%- customHtml %></div>
<!-- JavaScript logic (no output) -->
<% if (isAdmin) { %>
<a href="/admin">Admin Panel</a>
<% } %>
</body>
</html>
3.3 Passing Data to Templates
JavaScriptapp.get('/profile', (req, res) => {
const userData = {
pageTitle: 'User Profile',
username: 'john_doe',
isAdmin: true,
customHtml: '<em>Special message</em>'
};
res.render('welcome', userData);
});
3.4 Conditionals and Loops
EJS supports full JavaScript syntax inside <% %> tags:
HTML — EJS<!-- Conditional rendering -->
<% if (user.isLoggedIn) { %>
<p>Welcome back, <%= user.name %>!</p>
<% } else { %>
<a href="/login">Login</a>
<% } %>
<!-- Loops -->
<ul>
<% items.forEach(item => { %>
<li><%= item.name %> - $<%= item.price %></li>
<% }) %>
</ul>
<!-- Traditional for loop -->
<ul>
<% for(let i = 0; i < items.length; i++) { %>
<li><%= items[i] %></li>
<% } %>
</ul>
3.5 Using Partials (Includes)
EJS supports template partials for reusing components like headers and footers:
HTML — views/index.ejs<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
<%- include('partials/header') %>
<main>
<h1><%= title %></h1>
<p>Main content here</p>
</main>
<%- include('partials/footer') %>
</body>
</html>
Partial file (views/partials/header.ejs):
HTML — header.ejs<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
3.6 Complete EJS Example
Directory structure:
Directoryproject/
├── views/
│ ├── partials/
│ │ ├── header.ejs
│ │ └── footer.ejs
│ └── index.ejs
└── app.js
app.js:
JavaScript — app.jsconst express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Mouse', price: 25, inStock: true },
{ name: 'Keyboard', price: 75, inStock: false }
];
app.get('/', (req, res) => {
res.render('index', {
title: 'My Store',
products: products,
currentYear: new Date().getFullYear()
});
});
app.listen(3000);
views/index.ejs:
HTML — index.ejs<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<%- include('partials/header') %>
<h1><%= title %></h1>
<% if (products.length > 0) { %>
<ul>
<% products.forEach(product => { %>
<li>
<%= product.name %> - $<%= product.price %>
<% if (!product.inStock) { %>
<em>(Out of Stock)</em>
<% } %>
</li>
<% }) %>
</ul>
<% } else { %>
<p>No products available.</p>
<% } %>
<%- include('partials/footer', { year: currentYear }) %>
</body>
</html>
4. Handlebars
Handlebars is a logic-less templating engine that keeps templates clean by separating logic from presentation. It uses double curly braces and emphasizes minimal logic in templates.
4.1 Installation and Setup
Bashnpm install express-handlebars
JavaScriptconst express = require('express');
const exphbs = require('express-handlebars');
const path = require('path');
const app = express();
// Setup Handlebars as view engine
app.engine('handlebars', exphbs.engine());
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, 'views'));
4.2 Handlebars Syntax
| Syntax | Output | Use Case |
{{variable}} | Escaped HTML | Display text safely |
{{{variable}}} | Raw HTML | Unescaped HTML output |
{{#if condition}}...{{/if}} | Conditional block | Basic logic |
{{#each array}}...{{/each}} | Loop | Iterate over arrays |
4.3 Passing Data to Templates
JavaScriptapp.get('/dashboard', (req, res) => {
const dashboardData = {
title: 'Dashboard',
username: 'alice',
notifications: 3,
isPremium: true
};
res.render('dashboard', dashboardData);
});
Template (views/dashboard.handlebars):
HTML — dashboard.handlebars<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>Welcome, {{username}}!</h1>
{{#if isPremium}}
<p>Premium features unlocked! 🎉</p>
{{else}}
<a href="/upgrade">Upgrade to Premium</a>
{{/if}}
{{#if notifications}}
<p>You have {{notifications}} new notifications.</p>
{{/if}}
</body>
</html>
4.4 Layouts (Main Template)
Handlebars supports layouts — master templates that wrap your content. The {{{body}}} placeholder is where page-specific content gets injected.
views/layouts/main.handlebars:
HTML — main.handlebars<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}} | My App</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</nav>
</header>
<main>
{{{body}}} <!-- Page content goes here -->
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
Any page template (views/home.handlebars):
HTML — home.handlebars<h1>Home Page</h1>
<p>This content will be injected into the layout's {{{body}}}.</p>
4.5 Using Partials
Register partials for reusable components:
JavaScriptconst hbs = exphbs.create({
partialsDir: path.join(__dirname, 'views/partials')
});
app.engine('handlebars', hbs.engine);
Partial file (views/partials/card.handlebars):
HTML — card.handlebars<div class="card">
<h3>{{title}}</h3>
<p>{{content}}</p>
</div>
Using the partial:
Handlebars{{> card title="First Post" content="This is some content"}}
{{> card title="Second Post" content="More interesting content"}}
4.6 Complete Handlebars Example
JavaScript — app.jsconst express = require('express');
const exphbs = require('express-handlebars');
const path = require('path');
const app = express();
const hbs = exphbs.create({
partialsDir: path.join(__dirname, 'views/partials')
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
app.set('views', path.join(__dirname, 'views'));
app.get('/products', (req, res) => {
const products = [
{ id: 1, name: 'Laptop', price: 999, inStock: true },
{ id: 2, name: 'Mouse', price: 25, inStock: true },
{ id: 3, name: 'Keyboard', price: 75, inStock: false }
];
res.render('products', {
title: 'Product Catalog',
products: products,
showPrices: true
});
});
app.listen(3000);
HTML — layouts/main.handlebars<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
<header><h1>My Store</h1></header>
{{{body}}}
<footer>© 2024</footer>
</body>
</html>
HTML — products.handlebars<h2>{{title}}</h2>
{{#if products.length}}
<ul>
{{#each products}}
<li>
{{name}} - ${{price}}
{{#unless inStock}}
<span class="out-of-stock">(Out of Stock)</span>
{{/unless}}
</li>
{{/each}}
</ul>
{{else}}
<p>No products found.</p>
{{/if}}
5. Pug
Pug (formerly Jade) is a high-performance template engine with a minimal, indentation-based syntax. It's the default template engine for Express applications generated with express-generator.
5.1 Installation and Setup
Bashnpm install pug
JavaScriptconst express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
5.2 Pug Syntax Basics
Pug uses indentation (not closing tags) to define structure:
Pug//- This is a comment in Pug
doctype html
html
head
title My Page Title
body
h1 Welcome to Pug
p This is a paragraph.
a(href='/about') About Us
Rendered HTML:
HTML Output<!DOCTYPE html>
<html>
<head>
<title>My Page Title</title>
</head>
<body>
<h1>Welcome to Pug</h1>
<p>This is a paragraph.</p>
<a href="/about">About Us</a>
</body>
</html>
5.3 Variables and Interpolation
JavaScriptapp.get('/profile', (req, res) => {
res.render('profile', {
username: 'john_doe',
age: 28,
city: 'New York'
});
});
Pug template (views/profile.pug):
Pug — profile.pugdoctype html
html
head
title #{username}'s Profile
body
h1= username
p Age: #{age}
p City: #{city}
//- Interpolation in attributes
a(href=`/users/${username}`) View full profile
5.4 Attributes and Classes
Pug provides shorthand for classes and IDs:
Pug//- Standard attribute syntax
a(href='https://example.com', target='_blank') External Link
input(type='text', name='email', placeholder='Enter email')
//- Class and ID shorthand
div.container
#main-content.main-wrapper
button.btn.btn-primary Click Me
//- Renders to: <div class="container"></div>
//- <div id="main-content" class="main-wrapper"></div>
5.5 Conditionals and Loops
Conditionals:
Pug- var isLoggedIn = true
- var userRole = 'admin'
if isLoggedIn
p Welcome back!
if userRole === 'admin'
a(href='/admin') Admin Panel
else
a(href='/dashboard') Dashboard
else
a(href='/login') Login
Loops:
Pug- var items = ['Apple', 'Banana', 'Orange']
ul
each item in items
li= item
//- With index
ul
each item, index in items
li #{index + 1}: #{item}
5.6 Template Inheritance (Layouts)
Pug uses block and extends for layout inheritance.
layout.pug (base template):
Pug — layout.pugdoctype html
html
head
title My Site - #{title}
block scripts
script(src='/js/jquery.js')
body
header
nav
a(href='/') Home
a(href='/about') About
main
block content
block sidebar
footer
p © 2024 My Site
index.pug (child template):
Pug — index.pugextends layout
block content
h1= title
p Welcome to #{title}
each product in products
.product-card
h3= product.name
p Price: $#{product.price}
block sidebar
.sidebar
h3 Quick Links
ul
li: a(href='/latest') Latest Products
li: a(href='/sale') Sale Items
block scripts
script(src='/js/custom.js')
script(src='/js/analytics.js')
5.7 Complete Pug Example
JavaScript — app.jsconst express = require('express');
const path = require('path');
const app = express();
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
const blogPosts = [
{ id: 1, title: 'First Post', content: 'This is my first blog post!', published: true },
{ id: 2, title: 'Draft Post', content: 'Work in progress...', published: false },
{ id: 3, title: 'Express Guide', content: 'Learn Express.js step by step', published: true }
];
app.get('/blog', (req, res) => {
res.render('blog', {
title: 'My Blog',
posts: blogPosts,
currentUser: 'visitor'
});
});
app.listen(3000);
Pug — layout.pugdoctype html
html
head
title= title
link(rel='stylesheet', href='/css/style.css')
body
header
h1= title
p Welcome, #{currentUser}
block content
footer
p © 2024 All rights reserved
Pug — blog.pugextends layout
block content
main
h2 Latest Posts
if posts.length
each post in posts
if post.published
article.post
h3= post.title
p= post.content
hr
else
p No posts available.
6. Comparison and When to Use Which
| Feature |
EJS |
Handlebars |
Pug |
| Learning Curve | Easy (feels like HTML with JS) | Easy (minimal syntax) | Steeper (indentation-based) |
| Logic in Templates | Full JavaScript | Limited (logic-less by design) | Full JavaScript |
| Syntax Style | HTML with <% %> tags | {{ }} curly braces | Indentation-based, no tags |
| Layouts/Inheritance | Via partials (include) | Via layouts ({{{body}}}) | Via blocks (extends, block) |
| Best For | Developers wanting JS power in templates | Designers, clean separation of concerns | Concise syntax, performance |
| Readability | Very readable (close to HTML) | Very readable | Compact, requires familiarity |
When to choose:
- EJS: You want to write mostly standard HTML but need full JavaScript power in your templates. Great for teams coming from PHP or ASP.NET backgrounds.
- Handlebars: You want strict separation of logic and presentation. Templates stay clean and designer-friendly. Excellent for projects with dedicated front-end developers.
- Pug: You value concise syntax and don't mind learning new conventions. Great for small to medium projects. The default choice for Express Generator.
7. Conclusion
Express's static file serving and template engines give you powerful tools for building dynamic web applications:
- Static files with
express.static() let you serve CSS, JavaScript, and images efficiently with built-in caching and MIME type detection.
- EJS is perfect when you want HTML-like templates with embedded JavaScript logic — it's intuitive and powerful.
- Handlebars enforces clean separation of concerns with logic-less templates, layouts, and partials.
- Pug offers the most concise syntax with powerful inheritance features, though it has a steeper learning curve.