JavaScript URL utilities
The url module of modern-fns, a dependency-free JavaScript and TypeScript utility library.
import { setQuery, mergeQuery, parseUrl, joinUrl } from 'modern-fns';
import setQuery from 'modern-fns/url/setQuery';Every function accepts absolute, protocol-relative and relative URLs, and returns the same shape it received — a relative URL in, a relative URL out. new URL() throws on relative input; these do not.
Query values are parsed and serialised by the query engine, so arrays, nested objects and encoding behave identically in both modules.
parseUrl(url, options?)
Decompose a URL, with the query already parsed.
function parseUrl(url: string, options?: ParseOptions): ParsedUrl;
interface ParsedUrl {
protocol: string; // 'https' — no colon; '' when relative
hostname: string;
port: string;
host: string; // 'shop.com:8080'
origin: string;
pathname: string; // always starts with '/'
search: string; // '?a=1' or ''
query: QueryObject; // { a: 1 }
hash: string; // '#top' or ''
username: string;
password: string;
isAbsolute: boolean;
href: string;
}parseUrl('https://shop.com:8080/p?page=2&tags=a&tags=b#reviews');
// { protocol: 'https', hostname: 'shop.com', port: '8080', pathname: '/p',
// query: { page: 2, tags: ['a', 'b'] }, hash: '#reviews', isAbsolute: true, ... }
parseUrl('/products?page=2').isAbsolute; // falseEdge cases — protocol-relative input (//cdn.com/a.js) is absolute with an empty protocol. Malformed input degrades to a relative path instead of throwing. '' yields pathname: '/'.
buildUrl(options)
function buildUrl(options: {
protocol?: string;
host?: string;
port?: string | number;
path?: string;
query?: Record<string, unknown>;
hash?: string;
base?: string;
queryOptions?: StringifyOptions;
}): string;buildUrl({ host: 'api.shop.com', path: '/v1/products', query: { page: 2, tags: ['a', 'b'] } });
// 'https://api.shop.com/v1/products?page=2&tags=a&tags=b'
buildUrl({ path: '/products', query: { page: 2 }, hash: 'top' }); // '/products?page=2#top'
buildUrl({ base: 'https://api.com/v1', path: 'users', query: { a: 1 } });
// 'https://api.com/v1/users?a=1'Edge cases — protocol defaults to https when a host is given; without a host the result is relative. base wins over protocol/host and resolves path against it. A port is ignored when host already includes one. {} returns ''.
getQuery(url, key, options?) / getQueryParams(url, options?)
getQuery('/products?page=2', 'page'); // 2
getQueryParams('/products?page=2&tags=a&tags=b'); // { page: 2, tags: ['a', 'b'] }Edge cases — missing keys are undefined; a URL with no query yields {}. Values are coerced by the query parse rules.
setQuery(url, key, value, options?) / setQueryParams(url, params, options?)
setQuery('/products?page=2', 'sort', 'price'); // '/products?page=2&sort=price'
setQuery('/products?page=2', 'page', 3); // '/products?page=3'
setQuery('/p#top', 'a', 1); // '/p?a=1#top'
setQuery('/p', 'tags', ['a', 'b']); // '/p?tags=a&tags=b'
setQueryParams('/products?page=2&sort=price', { page: 1 }); // '/products?page=1'Edge cases — setQuery keeps the path, the other parameters, their order and the hash; existing keys are replaced in place, new keys appended. Passing undefined removes the key. setQueryParams replaces the whole query ({} clears it) — use mergeQuery to keep the rest.
mergeQuery(url, params, options?)
mergeQuery('/p?page=2&sort=price', { page: 3, q: 'shoes' }); // '/p?page=3&sort=price&q=shoes'
mergeQuery('/p?page=2&sort=price', { sort: undefined }); // '/p?page=2'Edge cases — undefined values delete their key, which makes "apply this filter patch to the current URL" one call. A nullish params is a no-op.
removeQuery(url, key) / hasQuery(url, key)
removeQuery('/p?page=2&sort=price', 'sort'); // '/p?page=2'
removeQuery('/p?a=1&b=2&c=3', ['a', 'c']); // '/p?b=2'
hasQuery('/p?q=', 'q'); // true — present but emptyEdge cases — removing the last parameter drops the ? entirely. Removing a missing key is a no-op.
setHash(url, hash) / removeHash(url)
setHash('/docs?page=2', 'install'); // '/docs?page=2#install'
setHash('/docs#old', '#new'); // '/docs#new'
removeHash('/docs?page=2#install'); // '/docs?page=2'Edge cases — the leading # is optional and never doubled; an empty string removes the fragment.
isSameUrl(urlA, urlB, options?)
Compare URLs semantically rather than as strings.
function isSameUrl(
urlA: string,
urlB: string,
options?: {
ignoreQueryOrder?: boolean; // default true
ignoreTrailingSlash?: boolean; // default true
ignoreHash?: boolean; // default false
ignoreQuery?: boolean; // default false
},
): boolean;isSameUrl('/p?a=1&b=2', '/p/?b=2&a=1'); // true
isSameUrl('https://x.com/p', 'https://x.com:443/p'); // true — default port
isSameUrl('/p#a', '/p#b'); // false
isSameUrl('/p#a', '/p#b', { ignoreHash: true }); // trueEdge cases — a relative URL never equals an absolute one, even if the paths match. Origins are compared including protocol and normalised port. Non-string input returns false.
Use it for — active-nav-link checks and "did the route really change?" guards.
joinUrl(...parts)
function joinUrl(...parts: Array<string | number | null | undefined>): string;joinUrl('https://api.com/', '/v1/', 'users'); // 'https://api.com/v1/users'
joinUrl('/base', 'a', 'b/'); // '/base/a/b/'
joinUrl('https://api.com', 'search?q=1'); // 'https://api.com/search?q=1'
joinUrl('/a?x=1', 'b?y=2'); // '/a/b?x=1&y=2'Edge cases — exactly one slash between parts, never a doubled one and never a missing one. Empty and nullish parts are skipped. Query strings and fragments found on any part are collected and moved to the end. A trailing slash on the final part is preserved. No parts returns ''.