The URL constructor
Now that you know the anatomy of a URL, let’s actually work with one in code. You could treat URLs as plain strings and use split(), indexOf(), and regex to pull them apart. People used to do that. It was painful, fragile, and full of edge cases. Thankfully, JavaScript gives us a built-in URL class that handles all of that cleanly.
Let’s see what it looks like and why you should reach for it every time.
Setting up
Before we start writing code, let’s set up a small project so you can run every example in this course.
mkdir url-playground
cd url-playground
npm init -y
npm install -D typescript @types/node tsx Create a tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "ES2022",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"rootDir": "./src",
"outDir": "dist",
"types": ["node"]
},
"include": ["src"]
} Create a src folder with a file to work in:
mkdir src
touch src/index.ts To run your code at any point during this course:
npx tsx src/index.ts That is all you need. Put each example in src/index.ts and run it with the command above.
Creating a URL object
You create a URL object by passing a URL string to the URL constructor:
const myUrl = new URL("https://api.example.com/users?page=2"); That’s it. You now have an object with every part of the URL available as a property:
console.log(myUrl.protocol); // "https:"
console.log(myUrl.hostname); // "api.example.com"
console.log(myUrl.pathname); // "/users"
console.log(myUrl.search); // "?page=2" myUrl is a variable that holds the URL object we just created. When we call new URL(...), JavaScript parses the string and breaks it into the pieces we learned about in the previous lesson. Each piece becomes a property on the object: protocol, hostname, pathname, search, and so on.
No regex. No string.split("?"). No indexOf. Just clean, readable property access.
Why not just use strings?
You could work with URLs as plain strings, but it gets messy fast. Let’s compare the two approaches.
The string way (fragile)
let url = "https://api.example.com/search";
url = url + "?query=" + encodeURIComponent("hello world"); This looks simple enough. But what happens if the URL already has a ? in it? What if there are already other parameters? You’d have to check every time, handle the ? vs & logic yourself, and manually encode special characters. One mistake and the URL breaks silently.
The URL way (clean)
const url = new URL("https://api.example.com/search");
url.searchParams.set("query", "hello world");
console.log(url.toString());
// "https://api.example.com/search?query=hello+world" Let’s walk through what happened here. First, we created a URL object from the base URL string. Then we used url.searchParams.set() to add a query parameter. searchParams is a special object attached to every URL (we’ll explore it deeply in the next section). The set method takes two arguments: the key ("query") and the value ("hello world").
Notice three things:
- No manual encoding. The space in “hello world” was handled automatically (turned into
+). - No worry about
?vs&.searchParamsknows whether this is the first parameter or not and uses the right separator. - Readable code. Anyone can understand what
searchParams.set("query", "hello world")does.
The second argument: base URLs
Sometimes you have a relative path (like /users/42) and need to combine it with a base URL. The URL constructor accepts an optional second argument for this:
const full = new URL("/users/42", "https://api.example.com");
console.log(full.href);
// "https://api.example.com/users/42" Here, the first argument is the path ("/users/42"), and the second argument is the base URL ("https://api.example.com"). JavaScript combines them into a complete URL for you.
This is incredibly useful when building API clients. You define the base URL once, then build all your endpoints from it:
const BASE = "https://api.example.com";
const usersUrl = new URL("/v2/users", BASE);
const productsUrl = new URL("/v2/products", BASE);
const ordersUrl = new URL("/v2/orders", BASE);
console.log(usersUrl.href); // "https://api.example.com/v2/users"
console.log(productsUrl.href); // "https://api.example.com/v2/products" BASE is just a string we defined to avoid repeating the full domain everywhere. Each new URL(...) call resolves the relative path against that base.
Why this matters for @hectoday/http
Inside @hectoday/http, the framework does exactly this when building test requests:
// From @hectoday/http source (setup.ts)
const url = new URL(path, "http://localhost"); When you call app.request("/users"), the framework combines your path with "http://localhost" to create a full, valid URL. The URL constructor does the heavy lifting. This is exactly how production apps handle internal routing.
What happens with invalid URLs?
What do you think happens if you pass something that isn’t a valid URL? Let’s try it.
try {
const bad = new URL("not-a-real-url");
} catch (error) {
console.log(error.message);
// "Invalid URL: not-a-real-url"
} The constructor throws an error. This is actually helpful. You find out immediately that something is wrong, rather than having a broken string silently passed around your app causing problems later.
However, if you provide a base URL, relative paths work just fine:
const ok = new URL("/just-a-path", "https://example.com");
console.log(ok.href); // "https://example.com/just-a-path" The string "/just-a-path" on its own isn’t a complete URL (no protocol, no host). But paired with the base URL as the second argument, it becomes valid. The base fills in the missing pieces.
Getting the string back with toString() and href
Once you’ve built or modified a URL object, you often need it as a string again. There are two ways to get it:
const url = new URL("https://example.com/page?x=1");
url.href; // "https://example.com/page?x=1"
url.toString(); // "https://example.com/page?x=1" Both return the exact same string. href is a property, toString() is a method. Use whichever feels more natural to you.
The URL object gives you structured access when you need it and a clean string when you don’t. In the next lesson, we’ll explore every property on the URL object and learn which ones you can change.
What does new URL('/api/data', 'https://example.com').href return?