npm package: @mjackson/headers
Description: A toolkit for working with HTTP headers in JavaScript
Author: Michael Jackson
Latest version: 0.11.1

# headers

Tired of manually parsing and stringifying HTTP header values in JavaScript? `headers` supercharges the standard `Headers` interface, providing a robust toolkit for effortless and type-safe header manipulation.

HTTP headers are packed with critical information—from content negotiation and caching directives to authentication tokens and file metadata. While the native `Headers` API provides a basic string-based interface, it leaves the complexities of parsing specific header formats (like `Accept`, `Content-Type`, or `Set-Cookie`) entirely up to you.

`headers` solves this by offering:

- **Type-Safe Accessors:** Interact with complex header values (e.g., media types, quality factors, cookie attributes) through strongly-typed properties and methods, eliminating guesswork and manual parsing.
- **Automatic Parsing & Stringification:** The library intelligently handles the parsing of raw header strings into structured objects and stringifies your structured data back into spec-compliant header values.
- **Fluent Interface:** Enjoy a more expressive and developer-friendly API for reading and writing header information.
- **Drop-in Enhancement:** As a subclass of the standard `Headers` object, it can be used anywhere a `Headers` object is expected, providing progressive enhancement to your existing code.
- **Individual Header Utilities:** For fine-grained control, use standalone utility classes for specific headers, perfect for scenarios outside of a full `Headers` object.

Unlock a more powerful and elegant way to work with HTTP headers in your JavaScript and TypeScript projects!

## Installation

```sh
npm install @mjackson/headers
```

## Overview

The following should give you a sense of what kinds of things you can do with this library:

```ts
import Headers from '@mjackson/headers';

let headers = new Headers();

// Accept
headers.accept = 'text/html, text/*;q=0.9';

headers.accept.mediaTypes; // [ 'text/html', 'text/*' ]
Object.fromEntries(headers.accept.entries()); // { 'text/html': 1, 'text/*': 0.9 }

headers.accept.accepts('text/html'); // true
headers.accept.accepts('text/plain'); // true
headers.accept.accepts('image/jpeg'); // false

headers.accept.getPreferred(['text/plain', 'text/html']); // 'text/html'

headers.accept.set('text/plain', 0.9);
headers.accept.set('text/*', 0.8);

headers.get('Accept'); // 'text/html,text/plain;q=0.9,text/*;q=0.8'

// Accept-Encoding
headers.acceptEncoding = 'gzip, deflate;q=0.8';

headers.acceptEncoding.encodings; // [ 'gzip', 'deflate' ]
Object.fromEntries(headers.acceptEncoding.entries()); // { 'gzip': 1, 'deflate': 0.8 }

headers.acceptEncoding.accepts('gzip'); // true
headers.acceptEncoding.accepts('br'); // false

headers.acceptEncoding.getPreferred(['gzip', 'deflate']); // 'gzip'

// Accept-Language
headers.acceptLanguage = 'en-US, en;q=0.9';

headers.acceptLanguage.languages; // [ 'en-us', 'en' ]
Object.fromEntries(headers.acceptLanguage.entries()); // { 'en-us': 1, en: 0.9 }

headers.acceptLanguage.accepts('en'); // true
headers.acceptLanguage.accepts('ja'); // false

headers.acceptLanguage.getPreferred(['en-US', 'en-GB']); // 'en-US'
headers.acceptLanguage.getPreferred(['en', 'fr']); // 'en'

// Accept-Ranges
headers.acceptRanges = 'bytes';

// Connection
headers.connection = 'close';

// Content-Type
headers.contentType = 'application/json; charset=utf-8';

headers.contentType.mediaType; // "application/json"
headers.contentType.charset; // "utf-8"

headers.contentType.charset = 'iso-8859-1';

headers.get('Content-Type'); // "application/json; charset=iso-8859-1"

// Content-Disposition
headers.contentDisposition =
  'attachment; filename="example.pdf"; filename*=UTF-8\'\'%E4%BE%8B%E5%AD%90.pdf';

headers.contentDisposition.type; // 'attachment'
headers.contentDisposition.filename; // 'example.pdf'
headers.contentDisposition.filenameSplat; // 'UTF-8\'\'%E4%BE%8B%E5%AD%90.pdf'
headers.contentDisposition.preferredFilename; // '例子.pdf'

// Cookie
headers.cookie = 'session_id=abc123; user_id=12345';

headers.cookie.get('session_id'); // 'abc123'
headers.cookie.get('user_id'); // '12345'

headers.cookie.set('theme', 'dark');
headers.get('Cookie'); // 'session_id=abc123; user_id=12345; theme=dark'

// Host
headers.host = 'example.com';

// If-None-Match
headers.ifNoneMatch = ['67ab43', '54ed21'];
headers.get('If-None-Match'); // '"67ab43", "54ed21"'

// Last-Modified
headers.lastModified = new Date();
// or headers.lastModified = new Date().getTime();
headers.get('Last-Modified'); // 'Fri, 20 Dec 2024 08:08:05 GMT'

// Location
headers.location = 'https://example.com';

// Referer
headers.referer = 'https://example.com/';

// Set-Cookie
headers.setCookie = ['session_id=abc123; Path=/; HttpOnly'];

headers.setCookie[0].name; // 'session_id'
headers.setCookie[0].value; // 'abc123'
headers.setCookie[0].path; // '/'
headers.setCookie[0].httpOnly; // true

// Modifying Set-Cookie attributes
headers.setCookie[0].maxAge = 3600;
headers.setCookie[0].secure = true;

headers.get('Set-Cookie'); // 'session_id=abc123; Path=/; HttpOnly; Max-Age=3600; Secure'

// Setting multiple cookies is easy, it's just an array
headers.setCookie.push('user_id=12345; Path=/api; Secure');
// or headers.setCookie = [...headers.setCookie, '...']

// Accessing multiple Set-Cookie headers
for (let cookie of headers.getSetCookie()) {
  console.log(cookie);
}
// session_id=abc123; Path=/; HttpOnly; Max-Age=3600; Secure
// user_id=12345; Path=/api; Secure
```

`Headers` can be initialized with an object config:

```ts
let headers = new Headers({
  contentType: {
    mediaType: 'text/html',
    charset: 'utf-8',
  },
  setCookie: [
    { name: 'session', value: 'abc', path: '/' },
    { name: 'theme', value: 'dark', expires: new Date('2021-12-31T23:59:59Z') },
  ],
});

console.log(`${headers}`);
// Content-Type: text/html; charset=utf-8
// Set-Cookie: session=abc; Path=/
// Set-Cookie: theme=dark; Expires=Fri, 31 Dec 2021 23:59:59 GMT
```

`Headers` works just like [DOM's `Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) (it's a subclass) so you can use them anywhere you need a `Headers`.

```ts
import Headers from '@mjackson/headers';

// Use in a fetch()
let response = await fetch('https://example.com', {
  headers: new Headers(),
});

// Convert from DOM Headers
let headers = new Headers(response.headers);

headers.set('Content-Type', 'text/html');
headers.get('Content-Type'); // "text/html"
```

If you're familiar with using DOM `Headers`, everything works as you'd expect.

`Headers` are iterable:

```ts
let headers = new Headers({
  'Content-Type': 'application/json',
  'X-API-Key': 'secret-key',
  'Accept-Language': 'en-US,en;q=0.9',
});

for (let [name, value] of headers) {
  console.log(`${name}: ${value}`);
}
// Content-Type: application/json
// X-Api-Key: secret-key
// Accept-Language: en-US,en;q=0.9
```

If you're assembling HTTP messages, you can easily convert to a multiline string suitable for using as a Request/Response header block:

```ts
let headers = new Headers({
  'Content-Type': 'application/json',
  'Accept-Language': 'en-US,en;q=0.9',
});

console.log(`${headers}`);
// Content-Type: application/json
// Accept-Language: en-US,en;q=0.9
```

## Individual Header Utility Classes

In addition to the high-level `Headers` API, `headers` also provides a rich set of primitives you can use to work with just about any complex HTTP header value. Each header class includes a spec-compliant parser (the constructor), stringifier (`toString`), and getters/setters for all relevant attributes. Classes for headers that contain a list of fields, like `Cookie`, are iterable.

If you need support for a header that isn't listed here, please [send a PR](https://github.com/mjackson/remix-the-web/pulls)! The goal is to have first-class support for all common HTTP headers.

### Accept

```ts
import { Accept } from '@mjackson/headers';

let header = new Accept('text/html;text/*;q=0.9');

header.has('text/html'); // true
header.has('text/plain'); // false

header.accepts('text/html'); // true
header.accepts('text/plain'); // true
header.accepts('text/*'); // true
header.accepts('image/jpeg'); // false

header.getPreferred(['text/html', 'text/plain']); // 'text/html'

for (let [mediaType, quality] of header) {
  // ...
}

// Alternative init styles
let header = new Accept({ 'text/html': 1, 'text/*': 0.9 });
let header = new Accept(['text/html', ['text/*', 0.9]]);
```

### Accept-Encoding

```ts
import { AcceptEncoding } from '@mjackson/headers';

let header = new AcceptEncoding('gzip,deflate;q=0.9');

header.has('gzip'); // true
header.has('br'); // false

header.accepts('gzip'); // true
header.accepts('deflate'); // true
header.accepts('identity'); // true
header.accepts('br'); // true

header.getPreferred(['gzip', 'deflate']); // 'gzip'

for (let [encoding, weight] of header) {
  // ...
}

// Alternative init styles
let header = new AcceptEncoding({ gzip: 1, deflate: 0.9 });
let header = new AcceptEncoding(['gzip', ['deflate', 0.9]]);
```

### Accept-Language

```ts
import { AcceptLanguage } from '@mjackson/headers';

let header = new AcceptLanguage('en-US,en;q=0.9');

header.has('en-US'); // true
header.has('en-GB'); // false

header.accepts('en-US'); // true
header.accepts('en-GB'); // true
header.accepts('en'); // true
header.accepts('fr'); // true

header.getPreferred(['en-US', 'en-GB']); // 'en-US'
header.getPreferred(['en', 'fr']); // 'en'

for (let [language, quality] of header) {
  // ...
}

// Alternative init styles
let header = new AcceptLanguage({ 'en-US': 1, en: 0.9 });
let header = new AcceptLanguage(['en-US', ['en', 0.9]]);
```

### Cache-Control

```ts
import { CacheControl } from '@mjackson/headers';

let header = new CacheControl('public, max-age=3600, s-maxage=3600');
header.public; // true
header.maxAge; // 3600
header.sMaxage; // 3600

// Alternative init style
let header = new CacheControl({ public: true, maxAge: 3600 });

// Full set of supported properties
header.public; // true/false
header.private; // true/false
header.noCache; // true/false
header.noStore; // true/false
header.noTransform; // true/false
header.mustRevalidate; // true/false
header.proxyRevalidate; // true/false
header.maxAge; // number
header.sMaxage; // number
header.minFresh; // number
header.maxStale; // number
header.onlyIfCached; // true/false
header.immutable; // true/false
header.staleWhileRevalidate; // number
header.staleIfError; // number
```

### Content-Disposition

```ts
import { ContentDisposition } from '@mjackson/headers';

let header = new ContentDisposition('attachment; name=file1; filename=file1.txt');
header.type; // "attachment"
header.name; // "file1"
header.filename; // "file1.txt"
header.preferredFilename; // "file1.txt"

// Alternative init style
let header = new ContentDisposition({
  type: 'attachment',
  name: 'file1',
  filename: 'file1.txt',
});
```

### Content-Type

```ts
import { ContentType } from '@mjackson/headers';

let header = new ContentType('text/html; charset=utf-8');
header.mediaType; // "text/html"
header.boundary; // undefined
header.charset; // "utf-8"

// Alternative init style
let header = new ContentType({
  mediaType: 'multipart/form-data',
  boundary: '------WebKitFormBoundary12345',
  charset: 'utf-8',
});
```

### Cookie

```ts
import { Cookie } from '@mjackson/headers';

let header = new Cookie('theme=dark; session_id=123');
header.get('theme'); // "dark"
header.set('theme', 'light');
header.delete('theme');
header.has('session_id'); // true

// Iterate over cookie name/value pairs
for (let [name, value] of header) {
  // ...
}

// Alternative init styles
let header = new Cookie({ theme: 'dark', session_id: '123' });
let header = new Cookie([
  ['theme', 'dark'],
  ['session_id', '123'],
]);
```

### If-None-Match

```ts
import { IfNoneMatch } from '@mjackson/headers';

let header = new IfNoneMatch('"67ab43", "54ed21"');

header.has('67ab43'); // true
header.has('21ba69'); // false

header.matches('"67ab43"'); // true

// Alternative init style
let header = new IfNoneMatch(['67ab43', '54ed21']);
let header = new IfNoneMatch({
  tags: ['67ab43', '54ed21'],
});
```

### Set-Cookie

```ts
import { SetCookie } from '@mjackson/headers';

let header = new SetCookie('session_id=abc; Domain=example.com; Path=/; Secure; HttpOnly');
header.name; // "session_id"
header.value; // "abc"
header.domain; // "example.com"
header.path; // "/"
header.secure; // true
header.httpOnly; // true
header.sameSite; // undefined
header.maxAge; // undefined
header.expires; // undefined

// Alternative init styles
let header = new SetCookie({
  name: 'session_id',
  value: 'abc',
  domain: 'example.com',
  path: '/',
  secure: true,
  httpOnly: true,
});
```

## Related Packages

- [`fetch-proxy`](https://github.com/mjackson/remix-the-web/tree/main/packages/fetch-proxy) - Build HTTP proxy servers using the web fetch API
- [`node-fetch-server`](https://github.com/mjackson/remix-the-web/tree/main/packages/node-fetch-server) - Build HTTP servers on Node.js using the web fetch API

## License

See [LICENSE](https://github.com/mjackson/remix-the-web/blob/main/LICENSE)