New to Rust? Grab our free Rust for Beginners eBook Get it free →
Render HTML file in Node.js and Express.js framework

Visitors to your Express app need an HTML page back, either a saved file or one built for that request. Express sends a finished file with res.sendFile() and builds a page from a template with res.render().
Here I’ll send a saved page and its stylesheet, then render a page per request with Pug.
What does Express send when a route handles HTML?
Express can return a saved HTML document with res.sendFile(), or generate a response from a template with res.render(). The first sends a file from disk. The second asks a view engine to combine a template with data.
| Approach | Use it when | Express sends |
|---|---|---|
| res.sendFile() | The HTML file is already complete | The file contents |
| res.render() | The page needs values that change per request | HTML generated from a view and its data |
Sending the document does not serve its linked files automatically. The browser requests a stylesheet or script by its own URL, so Express must map each asset URL to a file or route.
Prerequisites
Use a Node.js project with npm. The example uses CommonJS syntax and keeps browser files in public and Pug templates in views.
- Node.js and npm available in your shell.
- A project directory where you can create app.js, public, and views.
Initialize the project and install the two packages used by the complete example. The static route needs Express. Pug is used only for the dynamic view.
npm init -y
npm install express pug
mkdir -p public views
Step 1: Send the saved HTML page from a fixed root
Give res.sendFile() a fixed filename and an absolute root directory. Express accepts that relative filename only when root supplies the directory that contains it.
The / route sends the saved file. The /hello route passes a name to Pug, and the static middleware serves the stylesheet by URL.
const express = require('express');
const path = require('node:path');
const app = express();
const publicDir = path.join(__dirname, 'public');
app.set('view engine', 'pug');
app.set('views', path.join(__dirname, 'views'));
app.get('/', (_req, res) => {
res.sendFile('index.html', { root: publicDir });
});
app.get('/hello', (req, res) => {
const name = String(req.query.name || 'Ada');
res.render('hello', { name });
});
app.use(express.static(publicDir));
const port = Number(process.env.PORT || 3100);
app.listen(port, () => {
console.log(`Listening at http://localhost:${port}`);
});
The root option keeps the resolved file inside public, and the route always selects index.html rather than taking a filename from the request. The Express response API documents this path check. The res.sendFile() reference covers the method’s other file-transfer options.

The HTML links to /site.css, so express.static(publicDir) serves the stylesheet after the document response ends.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Express HTML response</title>
<link rel="stylesheet" href="/site.css">
</head>
<body>
<main>
<p class="eyebrow">Express response</p>
<h1>HTML file from Express</h1>
<p>This page came from a saved HTML file. Its stylesheet is a separate request.</p>
<a href="/hello?name=Ada">Open the dynamic greeting</a>
</main>
</body>
</html>
The browser reads the HTML response, then requests the stylesheet URL separately. Keep each referenced file under the public directory or add a route that serves it.
* { box-sizing: border-box; }
body { margin: 0; background: #f4f7fb; color: #182338; font: 18px/1.55 system-ui, sans-serif; }
main { max-width: 760px; margin: 12vh auto; padding: 40px; background: white; border: 1px solid #d6deea; }
.eyebrow { color: #3568a8; font-size: 14px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
h1 { margin: 8px 0 12px; font-size: 40px; line-height: 1.15; }
a { color: #165db4; }
Step 2: Render changing values with Pug
Use res.render() when the response depends on data. Express looks for the view in the configured views directory, passes the locals to Pug, and sends the generated HTML.
The Express template-engine guide shows how the view engine and directory settings work. This view uses escaped interpolation for the supplied name.
doctype html
html(lang="en")
head
meta(charset="utf-8")
meta(name="viewport", content="width=device-width, initial-scale=1")
title Dynamic greeting
link(rel="stylesheet", href="/site.css")
body
main
p.eyebrow Pug view
h1 Hello #{name}
p Express rendered this template with a value from the request.
a(href="/") Return to the saved HTML file
Pug escapes characters that could form an HTML tag when the template uses this interpolation. The executed request with an img-shaped name returned text rather than an image element, so keep untrusted values in escaped output.
Verify the HTML page and its assets
Start the server with PORT set to 3101, then request the page, stylesheet, and Pug route. Each response should match the resource the browser asked for.
export PORT=3101
node app.js
Run these requests separately. Their output shows the requested resource along with its HTTP status and content type.
curl -sS -w '\nHTTP %{http_code}, content type %{content_type}\n' http://localhost:3101/
curl -sS -w '\nHTTP %{http_code}, content type %{content_type}\n' http://localhost:3101/site.css
curl -sS -w '\nHTTP %{http_code}, content type %{content_type}\n' 'http://localhost:3101/hello?name=Ada'
| Request | Expected response | Observed result |
|---|---|---|
| / | HTML document | HTTP 200, HTML with UTF-8 |
| /site.css | Stylesheet | HTTP 200, CSS |
| /hello?name=Ada | Generated greeting | HTTP 200, HTML containing Hello Ada |

Troubleshoot missing files, styles, and views
A page can return HTML while a file path, asset URL, or template lookup fails. Match the response or error to the file and route that should handle it.
- If Express says “path must be absolute or specify root to res.sendFile,” pass an absolute path or a relative filename with an absolute root. Keep request data out of the filename unless it is checked against a fixed allowlist.
- If the page appears without its styling, request /site.css directly. A 404 means its URL does not map to the file, so check the public directory and the express.static path.
- If Express cannot find the view, confirm the file is views/hello.pug and that app.set(‘views’, …) points to that directory. Install Pug in the project that runs the server.
A missing route returns 404 rather than the HTML file. Check the exact URL before changing the file path.
curl -sS -w '\nHTTP %{http_code}, content type %{content_type}\n' http://localhost:3101/does-not-exist

Keep the route’s file path fixed
Keep the filename in the route fixed and let the root option define where Express can read it. When the page needs values that change between requests, render a template instead of building HTML from a request string.
From the project directory, start the server with:
export PORT=3101
node app.js
Express HTML file questions
A successful HTML response proves only that its document route worked. A stylesheet or view lookup has its own path and its own response.
Can res.sendFile() use a relative filename?
Yes. Pass a relative filename with an absolute root option. Express checks that the resolved path stays inside that root.
Why does the HTML page load without its CSS?
The HTML response does not automatically serve linked assets. Map the asset URL to a file with express.static() or another route.
When should I use res.render() instead of res.sendFile()?
Use res.sendFile() for an existing document. Use res.render() when a view engine must combine a template with request or server data.




