path and url
node:path builds filesystem paths; node:url parses and resolves HTTP and file:// URLs - conflating them causes broken imports, bad redirects, and SSRF bugs.
Recipe
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const dir = path.dirname(fileURLToPath(import.meta.url));
const config = path.join(dir, 'config.json');import { URL } from 'node:url';
const u = new URL('https://api.example.com/v1/users?page=2');
u.searchParams.set('limit', '50');When to reach for this:
- Resolving config files next to compiled
dist/output - Building outbound API URLs with query params
- Validating redirect targets against allowlists (SSRF prevention)
- Converting
import.meta.urlto absolute filesystem path
Working Example
import path from 'node:path';
import { fileURLToPath, URL } from 'node:url';
// ESM __dirname equivalent
const root = path.dirname(fileURLToPath(import.meta.url));
const dataFile = path.resolve(root, '..', 'data', 'users.json');
// API URL with safe query building
function usersEndpoint(base: string, page: number): string {
const url = new URL('/v1/users', base);
url.searchParams.set('page', String(page));
return url.href;
}
// SSRF guard - only allow https to known host
function assertAllowedTarget(href: string, allowedHosts: Set<string>): URL {
const parsed = new URL(href);
if (parsed.protocol !== 'https:') throw new Error('https required');
if (!allowedHosts.has(parsed.hostname)) throw new Error('host not allowed');
return parsed;
}What this demonstrates:
path.resolveandjoinprevent manual slash bugsURLconstructor resolves relative paths against base URLsearchParamsencodes query strings correctly- SSRF checks inspect
protocolandhostnamebeforefetch
Deep Dive
How It Works
- path is platform-aware (
win32vsposixinternals) -path.joinpicks separators. - URL follows WHATWG spec - same as browser
URL. - fileURLToPath / pathToFileURL bridge file URLs and filesystem paths.
- import.meta.resolve (experimental/stable per version) resolves specifiers like import.
path vs URL
| Task | Module |
|---|---|
| Join config dir | path.join |
| Parse query string on HTTP URL | URL |
| S3 object key | path.posix.join |
| Normalize redirect URL | new URL(input, base) |
TypeScript Notes
import path from 'node:path';
export function safeJoin(base: string, userSegment: string): string {
const resolved = path.resolve(base, userSegment);
if (!resolved.startsWith(path.resolve(base))) {
throw new Error('path traversal');
}
return resolved;
}Gotchas
- Using path for HTTP URLs - loses query/fragment semantics. Fix:
URLfor HTTP,pathfor disk. - Path traversal in
path.join(base, userInput)-../etc/passwd. Fix: resolve and verify prefix under base. - Assuming
url.pathnameis filesystem path on Windows - leading slash POSIX-like. Fix:fileURLToPathforfile:URLs only. - Double slashes in manual string concat - broken redirects. Fix:
new URL(relative, base). - IPv6 URLs without brackets - parse failures. Fix:
new URLhandles bracketed hosthttp://[::1]:8080.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
URL.canParse | Quick validity check | Need filesystem join |
path-to-regexp | Express route patterns | General URL parsing |
| Raw template strings | Never for user input | - |
| WHATWG URL in fetch | Client outbound | Local file paths |
FAQs
Where is __dirname in ESM?
path.dirname(fileURLToPath(import.meta.url)).
path.resolve vs join?
resolve absolutizes from right; join only concatenates - use resolve for user-relative paths with guard.
How to parse query strings?
new URL('http://x?' + qs) or url.searchParams - not manual split for encoded values.
file:// on Windows?
fileURLToPath converts to C:\... correctly with three-slash form.
URL and SSRF?
Validate hostname allowlist before server-side fetch - see SSRF Guards.
posix.join on Windows app?
Yes for S3 keys and URL-like routes stored as posix paths.
import.meta.url in tests?
Jest/Vitest set import meta - paths still resolve relative to test module.
Double encoding query params?
searchParams.set encodes once - do not pre-encode values.
Legacy url.parse?
Deprecated - use WHATWG URL constructor.
pathname trailing slash?
new URL('/api', 'https://x.com/v1/') - base path matters - test carefully.
Fastify route URLs?
Framework handles path params - use URL for outbound calls only.
Windows path in Docker?
Container paths are Linux - use posix inside container regardless of dev OS.
Related
- fs and fs/promises - open resolved paths
- ES Modules import - import.meta.url
- SSRF Guards - URL validation
- Built-in APIs Basics - overview
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.