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

Table of Contents

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.csshttp://localhost:3000/static/style.css
  • public/logo.pnghttp://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:

TagOutputUse Case
<%= variable %>Escaped HTMLDisplay user-generated content (safe)
<%- htmlContent %>Raw HTMLEmbed HTML strings (be cautious with user input)
<% code %>No outputRun 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

SyntaxOutputUse Case
{{variable}}Escaped HTMLDisplay text safely
{{{variable}}}Raw HTMLUnescaped HTML output
{{#if condition}}...{{/if}}Conditional blockBasic logic
{{#each array}}...{{/each}}LoopIterate 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>&copy; 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>&copy; 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 &copy; 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 &copy; 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 CurveEasy (feels like HTML with JS)Easy (minimal syntax)Steeper (indentation-based)
Logic in TemplatesFull JavaScriptLimited (logic-less by design)Full JavaScript
Syntax StyleHTML with <% %> tags{{ }} curly bracesIndentation-based, no tags
Layouts/InheritanceVia partials (include)Via layouts ({{{body}}})Via blocks (extends, block)
Best ForDevelopers wanting JS power in templatesDesigners, clean separation of concernsConcise syntax, performance
ReadabilityVery readable (close to HTML)Very readableCompact, 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.