How to Load CSS from a URL in Node.js
Fetch a remote stylesheet in Node.js with fetch, handle errors, save CSS, and choose between built-in APIs, node-fetch, and https.get.
To load CSS from a URL in Node.js, fetch the stylesheet and read the response as text. In current Node.js, the built-in fetch() API is the simplest approach:
const response = await fetch('https://example.com/styles.css');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const css = await response.text();
console.log(css);
This downloads the CSS bytes and decodes them into a JavaScript string. It does not apply the styles to a page, resolve every @import, or make Node.js import CSS as a native module. Those are separate tasks.
What “load CSS from a URL” means
Most Node.js code needs the stylesheet text so it can inspect, store, transform, inline, or send it elsewhere. An HTTP response with status 404 or 500 is still a completed response, so check response.ok before consuming the body. Network failures such as DNS errors, refused connections, and timeouts reject the promise instead.
Node.js’s global fetch() is a browser-compatible implementation. The API was added in Node.js 17.5.0 and 16.15.0 and became stable in Node.js 21.0.0. Verify the runtime deployed by your application against the Node.js fetch documentation.
Complete fetch example
Save this as load-css.mjs and run it with node load-css.mjs:
import { writeFile } from 'node:fs/promises';
const url = process.argv[2] ?? 'https://example.com/styles.css';
try {
const response = await fetch(url, {
headers: {
accept: 'text/css,text/plain;q=0.9,*/*;q=0.8'
},
signal: AbortSignal.timeout(15_000)
});
if (!response.ok) {
throw new Error(`CSS request failed: HTTP ${response.status} ${response.statusText}`);
}
const css = await response.text();
await writeFile('downloaded.css', css, 'utf8');
console.log(`Saved ${css.length} characters from ${response.url}`);
} catch (error) {
console.error(error instanceof Error ? error.message : error);
process.exitCode = 1;
}
AbortSignal.timeout() prevents a request from hanging forever. Choose a timeout appropriate for your network and workload.
cURL, Python, and Node.js equivalents
cURL
curl --fail --location \
--header 'Accept: text/css,text/plain;q=0.9,*/*;q=0.8' \
'https://example.com/styles.css' \
--output downloaded.css
Python
import requests
url = "https://example.com/styles.css"
response = requests.get(url, timeout=15)
response.raise_for_status()
css = response.text
with open("downloaded.css", "w", encoding=response.encoding or "utf-8") as file:
file.write(css)
print(f"Saved {len(css)} characters")
Node.js
const response = await fetch('https://example.com/styles.css');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const css = await response.text();
Save CSS safely and validate the response
A URL can return HTML, an error page, or an unexpectedly large body while still producing a successful HTTP status. Inspect headers and enforce a size limit before storing untrusted content.
import { writeFile } from 'node:fs/promises';
async function downloadCss(url, outputPath, maxBytes = 5 * 1024 * 1024) {
const response = await fetch(url, {
redirect: 'follow',
signal: AbortSignal.timeout(15_000)
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const contentType = response.headers.get('content-type') ?? '';
const declaredLength = Number(response.headers.get('content-length'));
if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
throw new Error(`Response is larger than ${maxBytes} bytes`);
}
if (contentType && !/(text\/css|text\/plain|application\/css)/i.test(contentType)) {
console.warn(`Unexpected content type: ${contentType}`);
}
const body = response.body;
if (!body) throw new Error('Response has no body');
const reader = body.getReader();
const chunks = [];
let total = 0;
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > maxBytes) throw new Error('CSS exceeds size limit');
chunks.push(value);
}
} finally {
reader.releaseLock();
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) {
bytes.set(chunk, offset);
offset += chunk.byteLength;
}
const css = new TextDecoder().decode(bytes);
await writeFile(outputPath, css, 'utf8');
return css;
}
downloadCss('https://example.com/styles.css', 'downloaded.css')
.then(css => console.log(`Saved ${css.length} characters`))
.catch(error => { console.error(error.message); process.exitCode = 1; });
Request options you may need
| Option | Use |
|---|---|
headers |
Send Accept, authorization, cookies, or a user agent when the server requires them. |
signal |
Cancel slow requests with AbortSignal.timeout() or your own AbortController. |
redirect |
Use follow (default), manual, or error according to your redirect policy. |
method |
Use GET for a stylesheet. A HEAD request can inspect headers but does not retrieve CSS. |
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10_000);
try {
const response = await fetch('https://example.com/styles.css', {
headers: {
accept: 'text/css',
'user-agent': 'my-css-loader/1.0'
},
signal: controller.signal
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const css = await response.text();
console.log(css);
} finally {
clearTimeout(timer);
}
Using node-fetch
Use node-fetch when your supported runtime lacks global fetch() or when your project standardizes on that package. Version 3 is ESM-only and cannot be loaded with require(); CommonJS projects that cannot migrate use the documented v2 line. See the node-fetch documentation.
ES modules with node-fetch v3
import fetch from 'node-fetch';
const response = await fetch('https://example.com/styles.css');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const css = await response.text();
console.log(css);
CommonJS with node-fetch v2
const fetch = require('node-fetch');
(async () => {
const response = await fetch('https://example.com/styles.css');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const css = await response.text();
console.log(css);
})();
node-fetch expects an absolute URL. Values such as /styles.css and //cdn.example.com/styles.css must first be resolved against a base URL.
const absoluteUrl = new URL('/styles.css', 'https://example.com/').href;
Using the lower-level https.get()
https.get() is useful when you need stream-level control or compatibility with older Node.js deployments. You must handle status codes, data chunks, and errors yourself. The Node.js HTTPS documentation describes the API.
import https from 'node:https';
function getCss(url) {
return new Promise((resolve, reject) => {
https.get(url, { headers: { accept: 'text/css' } }, response => {
if (response.statusCode < 200 || response.statusCode >= 300) {
response.resume();
reject(new Error(`HTTP ${response.statusCode}`));
return;
}
const chunks = [];
response.setEncoding('utf8');
response.on('data', chunk => chunks.push(chunk));
response.on('end', () => resolve(chunks.join('')));
response.on('error', reject);
}).on('error', reject);
});
}
const css = await getCss('https://example.com/styles.css');
console.log(css);
Fetching imported stylesheets
Reading one URL returns that file’s text only. CSS may reference more files through @import or relative assets such as fonts and images. To build a complete dependency graph, parse the CSS, resolve each reference with new URL(reference, stylesheetUrl), fetch recursively, and track visited URLs to prevent loops. A parser is safer than regular expressions because CSS syntax allows comments, strings, escaping, and nested constructs.
const childUrl = new URL('theme/base.css', 'https://example.com/css/main.css').href;
console.log(childUrl); // https://example.com/css/theme/base.css
Fetching is different from importing CSS
Native Node.js ESM does not directly import a stylesheet from an https: URL. This is separate from downloading remote data with fetch(). The ESM documentation explains that HTTP(S) imports require a deliberately configured custom loader; for ordinary processing, fetch the CSS as text instead.
// This is not a native Node.js remote CSS import:
// import styles from 'https://example.com/styles.css';
const css = await (await fetch('https://example.com/styles.css')).text();
Applying the CSS to a page
Node.js does not have a browser document or layout engine by itself. If your goal is to render a page, use a browser automation or rendering environment, load the page, and let that environment resolve stylesheets. If your goal is to inspect or transform CSS, keep it as text and use a CSS-aware parser or transformer chosen for your project.
Or skip the browser setup
If the real goal is a screenshot of a page after its CSS has loaded, ScreenshotNeo handles the browser capture through one request. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. An MCP server lets Claude, Cursor, and other MCP clients take screenshots. The Free plan includes 1,000 screenshots a month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Performance, reliability, and cost
- Reuse connections where your HTTP client supports keep-alive; avoid downloading the same immutable stylesheet repeatedly by caching it with a TTL or an
ETag/Last-Modifiedstrategy. - Set explicit timeouts and size limits. A remote server can be slow, redirect repeatedly, or return a very large body.
- Retry only transient failures such as connection resets or selected 5xx responses. Do not blindly retry 4xx responses.
- Validate and restrict destination URLs in server-side applications to reduce SSRF risk. Block private, loopback, link-local, and cloud metadata addresses when URLs come from users.
- Fetching CSS with Node itself has no Node.js usage charge; your network provider, proxy, hosting, or third-party API may impose costs.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
fetch is not defined |
The deployed Node.js version is too old or a different runtime is executing the code. | Upgrade to a supported Node.js release or install a compatible node-fetch version. |
| HTTP 404/403/500 | The URL is wrong, protected, or the server failed. | Check the URL, headers, authentication, and response status. Do not call response.text() as if the response were successful. |
| Invalid URL | A relative or protocol-relative URL was passed to fetch. | Resolve it with new URL(relative, base) and pass the resulting absolute URL. |
| Request hangs | The origin or network never completes the response. | Pass an AbortSignal timeout and handle the abort error. |
| CSS contains an HTML page | A proxy, login page, bot check, or error document was returned. | Inspect status, content type, redirects, and authentication before processing the body. |
| Styles do not appear in a browser | Downloading text does not apply it to a document. | Inject the text into a browser stylesheet or use a browser renderer that loads the page. |
require() fails for node-fetch |
node-fetch v3 is ESM-only. | Use ESM, dynamically import it, or use the v2 CommonJS line as documented by the project. |
FAQ
Should I use response.text() or response.arrayBuffer()?
Use text() for CSS inspection and transformation. Use bytes when you must preserve the original encoding or write the response without decoding it first.
Does fetch() follow redirects?
Yes, the default redirect mode is follow. Set redirect: 'error' when redirects are not acceptable, or inspect the final URL when redirect destinations matter.
Can I fetch a stylesheet from a private network?
Only if the Node.js process can reach it and your network policy permits it. Treat user-controlled URLs as untrusted and apply SSRF protections.
Can Node.js parse CSS automatically?
No. Node.js fetches the response; parsing, rewriting, minifying, and resolving imports require a CSS-aware tool or your own carefully scoped processing.
What is the simplest production pattern?
Use global fetch(), check response.ok, set a timeout, cap the body size, validate the content type when useful, and cache stylesheets that do not change frequently.


