build-url-ts
Version:
A small library that builds a URL given its components
560 lines (445 loc) β’ 13.2 kB
Markdown
# build-url-ts
[](https://github.com/meabed/build-url-ts/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/build-url-ts)
[](https://www.npmjs.com/package/build-url-ts)
[](https://bundlephobia.com/package/build-url-ts)
[](https://unpkg.com/browse/build-url-ts@latest/)
[](https://opensource.org/licenses/MIT)
A small, fast, zero-dependency library for building URLs with a fluent API. Fully typed for TypeScript and runs anywhere JavaScript does β Node.js, Bun, Deno, edge runtimes, and all modern browsers.
[](https://codesandbox.io/s/build-url-ts-demo-qer8y?fontsize=14&hidenavigation=1&theme=dark)
## Features
- π **Small & Fast** - Minimal footprint with zero dependencies (~1.3 KB min+gzip)
- π¦ **TypeScript Support** - Full TypeScript definitions for both ESM and CJS
- π **Dual Package** - First-class ESM + CommonJS entry points, plus a UMD bundle for CDNs
- π³ **Tree-shakeable** - Side-effect-free ESM; import only what you use
- π **Universal** - Node.js, Bun, Deno, edge runtimes, and all modern browsers
- π§ **Flexible** - Multiple ways to handle array query parameters
- β¨ **Clean API** - Simple and intuitive interface
- π‘οΈ **Safe** - Properly encodes URLs and handles edge cases
- π§ͺ **Well Tested** - Comprehensive test coverage with 137+ test cases
- π **Smart Merging** - Automatically merges with existing query parameters
## Installation
```bash
# bun
bun add build-url-ts
# npm
npm install build-url-ts
# yarn
yarn add build-url-ts
# pnpm
pnpm add build-url-ts
```
## Quick Start
```typescript
import { buildUrl } from 'build-url-ts';
const url = buildUrl('https://api.example.com', {
path: 'users/123',
queryParams: {
tab: 'profile',
limit: 10
}
});
// Result: https://api.example.com/users/123?tab=profile&limit=10
```
## Module Formats & Platforms
The package ships ESM, CommonJS, and a minified UMD bundle, with separate type
declarations for each module system, so it works the same everywhere.
### ESM β bundlers, Node, Bun
```typescript
import { buildUrl } from 'build-url-ts';
```
### CommonJS β `require()`
```javascript
const { buildUrl } = require('build-url-ts');
```
### Deno
```typescript
import { buildUrl } from 'npm:build-url-ts';
```
### Browser via CDN (`<script>`)
The UMD build exposes a global `buildUrl`:
```html
<script src="https://unpkg.com/build-url-ts"></script>
<script>
buildUrl.buildUrl('https://example.com', { path: 'about' });
</script>
```
> For production, pin a version and add Subresource Integrity, e.g.
> `<script src="https://unpkg.com/build-url-ts@6.2.0/dist/index.umd.min.js" integrity="sha384-β¦" crossorigin="anonymous"></script>`.
Or as an ES module, no bundler required:
```html
<script type="module">
import { buildUrl } from 'https://esm.sh/build-url-ts';
console.log(buildUrl('https://example.com', { path: 'about' }));
</script>
```
### Tree-shaking
The library is published as side-effect-free ESM (`"sideEffects": false`), so
bundlers drop everything you don't import. Pull in a single helper and only that
helper ends up in your bundle:
```typescript
import { buildQueryString } from 'build-url-ts';
// buildUrl, appendPath, buildHash are tree-shaken away
```
## API Reference
### `buildUrl(baseUrl?, options?)`
Builds a complete URL from components.
#### Parameters
- `baseUrl` (optional): `string | null` - The base URL
- `options` (optional): `IBuildUrlOptions` - URL building options
#### Options
```typescript
interface IBuildUrlOptions {
path?: string | number; // Single path segment
paths?: (string | number)[]; // Multiple path segments, appended in order
queryParams?: IQueryParams; // Query parameters object
hash?: string | number; // Hash/fragment identifier
lowerCase?: boolean; // Convert to lowercase
disableCSV?: boolean | IDisableCsvType; // Array handling
}
```
## Usage Examples
### Basic URL Building
```typescript
import { buildUrl } from 'build-url-ts';
// Simple URL with path
buildUrl('https://example.com', {
path: 'about'
});
// β https://example.com/about
// Multiple path segments (normalized and joined); `path` still works for one
buildUrl('https://example.com', {
paths: ['about', '/my/', '/cat']
});
// β https://example.com/about/my/cat
// With query parameters
buildUrl('https://example.com', {
path: 'search',
queryParams: {
q: 'typescript',
category: 'tutorials'
}
});
// β https://example.com/search?q=typescript&category=tutorials
// With hash
buildUrl('https://example.com', {
path: 'docs',
hash: 'installation'
});
// β https://example.com/docs#installation
// All combined
buildUrl('https://api.example.com', {
path: 'v1/users',
queryParams: {
role: 'admin',
active: true
},
hash: 'summary'
});
// β https://api.example.com/v1/users?role=admin&active=true#summary
```
### Working with Arrays
```typescript
// Default: Arrays as comma-separated values
buildUrl('https://api.example.com', {
queryParams: {
ids: [1, 2, 3]
}
});
// β https://api.example.com?ids=1,2,3
// Arrays as repeated parameters
buildUrl('https://api.example.com', {
queryParams: {
id: [1, 2, 3]
},
disableCSV: true
});
// β https://api.example.com?id=1&id=2&id=3
// Arrays with bracket notation
buildUrl('https://api.example.com', {
queryParams: {
id: [1, 2, 3]
},
disableCSV: 'array'
});
// β https://api.example.com?id[]=1&id[]=2&id[]=3
// Arrays with indexed notation (ascending)
buildUrl('https://api.example.com', {
queryParams: {
id: [1, 2, 3]
},
disableCSV: 'order_asc'
});
// β https://api.example.com?id[0]=1&id[1]=2&id[2]=3
// Arrays with indexed notation (descending)
buildUrl('https://api.example.com', {
queryParams: {
id: [1, 2, 3]
},
disableCSV: 'order_desc'
});
// β https://api.example.com?id[2]=1&id[1]=2&id[0]=3
```
### Case Transformation
```typescript
// Convert to lowercase
buildUrl('https://example.com', {
path: 'About',
hash: 'Contact',
queryParams: {
Filter: 'NEW'
},
lowerCase: true
});
// β https://example.com/about?filter=new#contact
```
### Building Partial URLs
```typescript
// Query string only
buildUrl(null, {
queryParams: {
page: 1,
limit: 20
}
});
// β ?page=1&limit=20
// Path only
buildUrl(null, {
path: 'users/profile'
});
// β /users/profile
// Hash only
buildUrl(null, {
hash: 'top'
});
// β #top
// Using options as first parameter
buildUrl({
path: 'api/v2',
queryParams: {
format: 'json'
}
});
// β /api/v2?format=json
```
### Handling Special Values
```typescript
// Null values become empty strings
buildUrl('https://api.example.com', {
queryParams: {
name: 'John',
age: null
}
});
// β https://api.example.com?name=John&age=
// Undefined values are omitted
buildUrl('https://api.example.com', {
queryParams: {
name: 'John',
age: undefined
}
});
// β https://api.example.com?name=John
// Number values
buildUrl('https://api.example.com', {
path: 404,
queryParams: {
code: 0,
retry: 3
}
});
// β https://api.example.com/404?code=0&retry=3
// Boolean values
buildUrl('https://api.example.com', {
queryParams: {
active: true,
deleted: false
}
});
// β https://api.example.com?active=true&deleted=false
// Date objects
const date = new Date('2024-01-01T00:00:00Z');
buildUrl('https://api.example.com', {
queryParams: {
created: date
}
});
// β https://api.example.com?created=Mon%20Jan%2001%202024...
// Nested objects (automatically stringified)
buildUrl('https://api.example.com', {
queryParams: {
filter: { status: 'active', role: 'admin' }
}
});
// β https://api.example.com?filter=%7B%22status%22%3A%22active%22%2C%22role%22%3A%22admin%22%7D
```
## Advanced Usage
### Using Individual Functions
The library also exports individual functions for more granular control:
```typescript
import {
buildQueryString,
appendPath,
buildHash
} from 'build-url-ts';
// Build query string only
const qs = buildQueryString({
search: 'typescript',
limit: 10
});
// β ?search=typescript&limit=10
// Append path to URL
const urlWithPath = appendPath('users/123', 'https://api.example.com');
// β https://api.example.com/users/123
// Build hash fragment
const hash = buildHash('section-2');
// β #section-2
```
### TypeScript Types
```typescript
import type {
IQueryParams,
IBuildUrlOptions,
IDisableCsvType
} from 'build-url-ts';
// Custom query params type
interface MyParams extends IQueryParams {
userId: number;
tags?: string[];
active?: boolean;
}
const options: IBuildUrlOptions = {
path: 'api/users',
queryParams: {
userId: 123,
tags: ['admin', 'verified']
} as MyParams
};
```
## URL Encoding
All values are properly encoded for URLs:
```typescript
buildUrl('https://example.com', {
queryParams: {
name: 'John Doe',
email: 'john@example.com',
message: 'Hello & goodbye!',
unicode: 'δ½ ε₯½δΈη'
}
});
// β https://example.com?name=John%20Doe&email=john%40example.com&message=Hello%20%26%20goodbye!&unicode=%E4%BD%A0%E5%A5%BD%E4%B8%96%E7%95%8C
```
## Edge Cases
The library handles various edge cases gracefully:
```typescript
// Empty or missing base URL
buildUrl('', { path: 'api' }); // β /api
buildUrl(null, { path: 'api' }); // β /api
buildUrl(undefined, { path: 'api' }); // β /api
// Trailing slashes
buildUrl('https://example.com/', { path: '/users' });
// β https://example.com/users (no double slash)
// Empty values
buildUrl('https://example.com', {
path: '', // ignored
hash: '', // ignored
queryParams: {
empty: '', // included as empty
zero: 0, // included as "0"
false: false // included as "false"
}
});
// β https://example.com?empty=&zero=0&false=false
// URLs with existing query parameters (automatic merging)
buildUrl('https://example.com?existing=param', {
queryParams: {
new: 'value'
}
});
// β https://example.com?existing=param&new=value
// URLs with ports and authentication
buildUrl('http://user:pass@localhost:3000', {
path: 'api/secure'
});
// β http://user:pass@localhost:3000/api/secure
// Special protocols
buildUrl('file:///home/user/data', {
queryParams: { version: 2 }
});
// β file:///home/user/data?version=2
// Internationalized domain names and emoji
buildUrl('https://δΎγ.jp', {
queryParams: {
search: 'π',
text: 'δ½ ε₯½'
}
});
// β https://δΎγ.jp?search=%F0%9F%94%8D&text=%E4%BD%A0%E5%A5%BD
// Empty arrays are omitted
buildUrl('https://api.example.com', {
queryParams: {
ids: [],
name: 'test'
}
});
// β https://api.example.com?name=test
// Arrays with null/undefined values
buildUrl('https://api.example.com', {
queryParams: {
items: ['one', null, undefined, 'four']
},
disableCSV: true
});
// β https://api.example.com?items=one&items=&items=four
// (undefined values are filtered out)
```
## Migration Guide
### From `build-url`
This library is a TypeScript fork of the original `build-url` package with improvements:
```javascript
// Before (build-url)
var buildUrl = require('build-url');
// After (build-url-ts)
import { buildUrl } from 'build-url-ts';
```
The API remains fully compatible, so you can simply replace the import.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request
## Development
This project uses [Bun](https://bun.sh) as its package manager, test runner, and script runner.
```bash
# Install dependencies
bun install
# Run tests
bun test
# Run tests in watch mode
bun run test:watch
# Run tests with coverage
bun run test:coverage
# Build the library (CJS + ESM + type declarations)
bun run build
# Lint and format (Biome)
bun run lint
bun run check
# Type checking
bun run typecheck
```
### Test Coverage
The library has comprehensive test coverage with 137+ test cases covering:
- Basic URL building scenarios
- Various array handling modes
- Special characters and encoding
- Edge cases and error handling
- Protocol support (http, https, file, ftp, etc.)
- Internationalization and emoji support
- Query parameter merging
- Date and object serialization
## License
This is licensed under MIT License. [See details](LICENSE)
## Acknowledgments
This is a TypeScript enhancement of the original [build-url](https://github.com/steverydz/build-url) library by [Steve Rydz](https://github.com/steverydz).