# urlcat trades size for one runtime dependency

> urlcat is a TypeScript URL builder that fills path templates and encodes parameters, so a request URL can be written on one line. The current v3 line weighs 10.5KB minified and gzipped against 0.8KB for the legacy v2, because it now depends on qs.

**balazsbotond/urlcat** — A URL builder library for JavaScript.

- Repository: https://github.com/balazsbotond/urlcat
- Website: https://urlcat.org
- Stars: 1,869 · Forks: 61
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/balazsbotond-urlcat

## The bug urlcat is aimed at is a slash and a missing escape

The README's motivating example is a function that builds a request URL from a base constant with a trailing slash:

```js
const requestUrl = `${API_URL}/users/${id}/blogs/${blogId}/posts?limit=${limit}&offset=${offset}`;
```

Two defects are named. The trailing slash on API_URL produces a URL containing duplicate slashes, and the interpolated values are never passed through encodeURIComponent. The author's own fix using the built-in URL and URLSearchParams classes does remove the duplicate slash and escapes the query, but path parameters still have to be escaped by hand, and the resulting function is several lines longer than the call it replaces.

urlcat compresses that to a single call, filling the path template and encoding every value, whether it lands in a path segment or in the query string.

```js
const requestUrl = urlcat(API_URL, '/users/:id/posts', { id, limit, offset });
```

The library's own description of what it guarantees is narrow: there is always exactly one slash and one question mark character between concatenated parts. That is the contract, and it is testable in a way that "builds correct URLs" is not.

## v3 grew thirteenfold over v2 to gain a query string parser

The feature table in the README is the most useful thing in it, because it makes the project's main trade-off explicit in two rows. Version 3 is the latest version and is described as actively supported, with v2 as the legacy version, and both promise the same friendly API.

Then the sizes. v3 is 10.5KB minified and gzipped and carries one dependency, which package.json names as qs at ^6.16.0. v2 is 0.8KB minified and gzipped with no dependencies at all.

So the current line of a library whose README describes it as "tiny" is more than an order of magnitude larger than the line it replaced. v3 therefore adds both bundle weight and a runtime dependency even when an application does not use that dependency.

The version history explains the rest. v3.0.0 landed on 2022-11-19, v3.1.0 on 2023-01-17 and v3.2.0 on 2026-10-12, which is the release that brought the current package layout. v3.1.0 is also the point where ES module support arrived.

## Installing and importing, in three module systems

The package is distributed through npm, and zip downloads are available since v3.0.0:

```bash
npm install --save urlcat
```

CommonJS has three separate import shapes because the default export is nested under `.default` in the compiled CommonJS build. To build full URLs:

```ts
const urlcat = require('urlcat').default;
```

The named utilities query, subst and join sit on the module object, and importing all of them together needs the explicit rename:

```ts
const { default: urlcat, query, subst, join } = require('urlcat');
```

ES modules and TypeScript both use the flat form the README shows in one line each:

```ts
import urlcat, { query, subst, join } from 'urlcat';
```

Node 14 and above are officially supported, and CI checks the packed package on every even-numbered Node release line from 14 up to the current one. TypeScript 2.1 and above is supported, and the library ships its own type definitions, so no @types package is needed.

## Path templates share one flat object with the query string

urlcat exposes three overloads for the default export, two of which accept a ParamMap, which the type section defines as `Record<string, any>`. You can pass a base URL and a path template, a base and a template, or a single base template plus params.

The design choice to notice is that path parameters and query parameters share one object. Nothing marks which keys belong in the path, so the template does that work:

```ts
urlcat('http://example.com/', '/users/:userId/posts/:postId/comments', { userId: 123, postId: 987, authorId: 456, limit: 10, offset: 120 })
```

produces `/users/123/posts/987/comments?authorId=456&limit=10&offset=120`. Keys that appear in the template are substituted into the path, and the remainder become query parameters. Escaping is automatic in both positions: a title containing an ampersand and quote marks comes back percent-encoded.

The cost of one flat map is that a typo in a template placeholder does not fail loudly. A key that does not match a placeholder silently becomes a query parameter, so the URL stays valid and the request quietly carries an unexpected filter.

## Empty path segments follow RFC 3986, not intuition

The API section closes with a note about empty path segments that links to RFC 3986. That is the one place where the library's behaviour can surprise someone who expects a missing parameter to collapse a segment.

The documented examples make the base-slash handling easy to check. Both `urlcat('https://api.example.com', '/users')` and `urlcat('https://api.example.com/', '/users')` produce `https://api.example.com/users`, which is exactly the duplicate-slash defect from the opening example, closed off. That normalisation is the reason the library exists, and it is the behaviour most worth writing a test for in your own code.

The published surface is small: query, subst and join as named utilities, plus the default export. Small surfaces age well, and they also mean there is little to configure and nothing to learn beyond the template syntax.

## Where it stops being the right answer

Three limits show up in practice. The first is bundle size, covered above. The second is the flat ParamMap, which makes the library a poor fit for APIs whose parameters are deeply nested or where you need explicit control over ordering and repeated keys.

The third is release packaging. package.json ships only the dist directory and declares separate import and require entries with their own type files, which is correct but means the module resolution depends on the exports map. That is why the CommonJS default export lives under `.default`, a detail that trips up codebases migrating from v2, where the API was identical but the import shape was not.

There is also a build-time dependency on Node tooling: the scripts use tsup for building, vitest for tests, eslint for linting, and both @arethetypeswrong/cli and publint in the check:package step that runs before publish. Contributors inherit that toolchain. Consumers inherit none of it.

## Conclusion

urlcat is worth taking for codebases that build many API URLs by hand and already ship a query string parser, because the template syntax and the ParamMap type remove a class of escaping bugs at low review cost. It is a poor choice for a browser bundle that cares about bytes, where the v3 line costs 10.5KB minified and gzipped against 0.8KB for the dependency-free v2. Before adopting, check whether your project already depends on qs, since v3 lists it as its only runtime dependency and the README does not say the library works without it.

## FAQ

### How do I install urlcat and import it?

Run `npm install --save urlcat`, then use `import urlcat from 'urlcat'` in ES module or TypeScript code. Under CommonJS the default export sits at `require('urlcat').default`.

### What are the differences between urlcat v3 and the legacy v2?

Both expose the same API, but v3 is 10.5KB minified and gzipped with one dependency, qs, while v2 is 0.8KB with no dependencies. The README marks v3 as the supported line and v2 as legacy.

### Does urlcat escape values used in the path?

Yes. The README states that the library escapes all parameters and concatenates parts so that exactly one slash and one question mark appear between them, and its examples show characters like ampersands and quotes percent-encoded in a path segment.

### Which Node and TypeScript versions does urlcat support?

Node 14 and above are officially supported, with CI checking the packed package on every even-numbered Node release line from 14 upward. TypeScript 2.1 and above is supported, and the package ships its own type definitions.

## Sources

- [balazsbotond/urlcat on GitHub](https://github.com/balazsbotond/urlcat)
- [License: MIT](https://github.com/balazsbotond/urlcat/blob/master/LICENSE)
- [Project website](https://urlcat.org)
- [README](https://github.com/balazsbotond/urlcat/blob/master/README.md)
- [Releases](https://github.com/balazsbotond/urlcat/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/balazsbotond-urlcat
