π¦ The url Module
url is a built-in Node.js module for parsing and working with URLs β pulling out the protocol, host, path, and query string instead of slicing up a raw string yourself.
import { URL } from 'node:url';π‘ URL and URLSearchParams are also available as globals in Node.js β this import is only needed if you want to be explicit about where they come from.
π Parsing a URL
const myURL = new URL('<https://codeveda.dev:8080/blog/post?id=42&sort=asc#comments>');
console.log(myURL);new URL(string)turns a URL string into a URL object, with each part already broken out for you.This is the modern, standard way to work with URLs in Node β it's the same
URLclass browsers use.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Parsing a URL</title>
</head>
<body>
<div class="url-info" id="urlinfo">
<style>
.url-info {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft: #e3ebfd;
--line: #ddd6c9;
--display-font: 'Fraunces', Georgia, serif;
--body-font: 'Inter', system-ui, sans-serif;
--mono-font: 'IBM Plex Mono', monospace;
--c-protocol: #2563eb;
--c-host: #16a34a;
--c-port: #9333ea;
--c-pathname: #d97706;
--c-search: #0d9488;
--c-hash: #dc2626;
max-width: 880px;
margin: 0 auto;
box-sizing: border-box;
padding: 2rem;
font-family: var(--body-font);
color: var(--ink);
background: var(--paper);
}
.url-info[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1c212b;
--accent: #5b8def;
--accent-soft: #202942;
--line: #2c313d;
--c-protocol: #5b8def;
--c-host: #4ade80;
--c-port: #c084fc;
--c-pathname: #fbbf24;
--c-search: #2dd4bf;
--c-hash: #f87171;
}
.url-info * { box-sizing: border-box; }
.ur-eyebrow {
font-family: var(--mono-font); font-size: 0.68rem; letter-spacing: 0.14em; text-transform: uppercase;
color: var(--accent); text-align: center; margin: 1.5rem 0 0.3rem;
}
.ur-title {
font-family: var(--display-font); font-size: clamp(1.4rem, 5vw, 1.85rem); text-align: center;
margin: 0 0 0.5rem; letter-spacing: -0.01em;
}
.ur-sub { color: var(--ink-soft); font-size: 0.88rem; text-align: center; margin: 0 0 1.6rem; }
/* ---- URL line ---- */
.ur-url-wrap {
border: 1px solid var(--line); border-radius: 8px; background: #1e1e1e;
padding: 0.9rem 1rem; margin-bottom: 1rem; overflow-x: auto;
}
.ur-url-line { font-family: var(--mono-font); font-size: 0.82rem; white-space: pre; }
.ur-part { color: #d4d4d8; transition: color 0.2s, opacity 0.2s, font-weight 0.2s; }
.ur-part.is-sep { color: #6b7280; }
.ur-url-line.has-selection .ur-part:not(.is-highlighted):not(.is-sep) { opacity: 0.35; }
.ur-part.is-highlighted { font-weight: 700; color: var(--tab-color); }
/* ---- tabs ---- */
.ur-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; }
.ur-tab {
font-family: var(--mono-font); font-size: 0.72rem; font-weight: 600; padding: 0.4rem 0.75rem;
border-radius: 14px; border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); cursor: pointer;
display: flex; align-items: center; gap: 0.35rem;
}
.ur-tab-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.ur-tab.is-active { border-color: var(--tab-color); background: color-mix(in srgb, var(--tab-color) 14%, var(--paper)); color: var(--tab-color); }
.ur-hint { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin: 1.1rem 0 2rem; }
@media (prefers-reduced-motion: reduce) {
.url-info *, .url-info *::before, .url-info *::after { transition-duration: 0.001ms !important; animation-duration: 0.001ms !important; }
}
</style>
<div class="ur-url-wrap">
<div class="ur-url-line" id="urLine"></div>
</div>
<div class="ur-tabs" id="urTabs"></div>
<p class="ur-hint">Tap a tab to highlight that part in the URL above. Tap it again to clear.</p>
</div>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Fraunces:wght@500;600&family=Inter:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet">
<script>
(function () {
var root = document.getElementById('urlinfo');
if (!root) return;
var bodyAttr = document.body.getAttribute('data-darkmode');
var dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
var match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) root.setAttribute('data-theme', 'dark');
var urlLine = root.querySelector('#urLine');
var tabsEl = root.querySelector('#urTabs');
var PART_LABELS = { protocol: 'protocol', host: 'host', port: 'port', pathname: 'pathname', search: 'search', hash: 'hash' };
function esc(s) { return String(s).replace(/&/g, '&').replace(/</g, '<'); }
function selectPart(part) {
var tabs = tabsEl.querySelectorAll('.ur-tab');
var alreadyActive = false;
tabs.forEach(function (t) {
if (t.getAttribute('data-part') === part && t.classList.contains('is-active')) alreadyActive = true;
});
var target = alreadyActive ? null : part;
tabs.forEach(function (t) { t.classList.toggle('is-active', t.getAttribute('data-part') === target); });
var parts = urlLine.querySelectorAll('.ur-part[data-part]');
parts.forEach(function (p) { p.classList.toggle('is-highlighted', p.getAttribute('data-part') === target); });
urlLine.classList.toggle('has-selection', !!target);
}
var u = new URL('https://codeveda.dev:8080/blog/post?id=42&sort=asc#comments');
var segs = [];
segs.push({ text: u.protocol, part: 'protocol' });
segs.push({ text: '//', sep: true });
segs.push({ text: u.hostname, part: 'host' });
if (u.port) {
segs.push({ text: ':', sep: true });
segs.push({ text: u.port, part: 'port' });
}
segs.push({ text: u.pathname, part: 'pathname' });
if (u.search) segs.push({ text: u.search, part: 'search' });
if (u.hash) segs.push({ text: u.hash, part: 'hash' });
urlLine.innerHTML = segs.map(function (s) {
if (s.sep) return '<span class="ur-part is-sep">' + esc(s.text) + '</span>';
return '<span class="ur-part" data-part="' + s.part + '" style="--tab-color:var(--c-' + s.part + ');">' + esc(s.text) + '</span>';
}).join('');
var presentParts = [];
segs.forEach(function (s) { if (!s.sep && presentParts.indexOf(s.part) === -1) presentParts.push(s.part); });
tabsEl.innerHTML = presentParts.map(function (p) {
return '<button type="button" class="ur-tab" data-part="' + p + '" style="--tab-color:var(--c-' + p + ');">' +
'<span class="ur-tab-dot" style="background:var(--c-' + p + ');"></span>' + PART_LABELS[p] + '</button>';
}).join('');
tabsEl.querySelectorAll('.ur-tab').forEach(function (tab) {
tab.addEventListener('click', function () { selectPart(tab.getAttribute('data-part')); });
});
})();
</script>
</body>
</html>π·οΈ Reading the Parts
console.log(myURL.protocol); // 'https:'
console.log(myURL.hostname); // 'codeveda.dev'
console.log(myURL.port); // '8080'
console.log(myURL.pathname); // '/blog/post'
console.log(myURL.search); // '?id=42&sort=asc'
console.log(myURL.hash); // '#comments'
console.log(myURL.href); // the full URL as a stringprotocolincludes the trailing colon ('https:', not'https').hostnameis just the domain;hostwould include the port too ('codeveda.dev:8080').searchis the raw query string, including the?.hrefgives you back the complete URL β handy after you've edited one of the other properties.
π¦ Handling Form Data & Query Strings β The Modern Way
Both of these boil down to the same thing: reading key=value pairs. Node's current APIs handle both with the same tools β no manual string-splitting, and no need for the older querystring module.
URL/URLSearchParamsβ global in Node.js, no import needednode:stream/consumersβ reads a request stream into text without manually collecting chunksnode:httpβ using thenode:prefix is the current recommended way to import core modules
πΉ Reading Form Data (POST Body)
π index.html:
<form action="/submit" method="POST">
<input name="name" placeholder="Enter name">
<button type="submit">Submit</button>
</form>π¨βπ» server.js:
import { createServer } from 'node:http';
import { text } from 'node:stream/consumers';
const server = createServer(async (req, res) => {
if (req.method === 'POST' && req.url === '/submit') {
const body = await text(req); // 'name=Varsha'
console.log('Raw Body:', body);
const params = new URLSearchParams(body);
const name = params.get('name');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`You submitted: ${name}`);
} else {
res.writeHead(404);
res.end('Page not found');
}
});
server.listen(3000, () => {
console.log('Server listening at <http://localhost:3000>');
});
Output β submit the form, and the response is:
You submitted: Varshatext(req), fromnode:stream/consumers, reads the whole request stream and gives you back a string β noreq.on('data')/req.on('end')bookkeeping needed.A form-urlencoded body (
name=Varsha) is in the exact same format as a URL query string, soURLSearchParamsparses it directly.params.get('name')reads the value safely β it handles multiple fields, special characters, and missing values correctly, unlike manually splitting on=.
πΉ Reading Query Parameters
A mini server that reads query parameters from a URL like:
<http://localhost:3000/greet?name=Varsha>π server.js:
import { createServer } from 'node:http';
const server = createServer((req, res) => {
const myUrl = new URL(req.url, `http://${req.headers.host}`);
if (myUrl.pathname === '/greet') {
const name = myUrl.searchParams.get('name') || 'Guest';
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(`Hello, ${name}!`);
} else {
res.writeHead(404);
res.end('Page not found');
}
});
server.listen(3000, () => {
console.log('Server running on <http://localhost:3000>');
});Output β visiting http://localhost:3000/greet?name=Varsha in a browser returns:
Hello, Varsha!req.urlonly holds the path and query string (/greet?name=Varsha), not a full URL β sonew URL()needs a base, which is whyreq.headers.hostis passed as the second argument.myUrl.pathnameisolates the route, separate from the query string, so you can branch on it.myUrl.searchParams.get('name')reads the parameter β the sameURLSearchParamsAPI used for the form body above.The
|| 'Guest'fallback covers a missing?name=so the server doesn't respond withHello, null!.
π‘ Why This Is the Current Approach
One API β
URLSearchParamsβ handles key-value pairs whether they come from a form body or a URL's query string.node:stream/consumersreplaces manual chunk-collecting (req.on('data')/req.on('end')) with a singleawait text(req)call.URLandURLSearchParamsare globals in Node.js β no import required for them specifically.The
node:prefix on imports (node:http,node:stream/consumers) is the current recommended style for built-in modules, making it explicit they're core Node APIs and not npm packages.No dependency on the older
querystringmodule anywhere in either example.