# npm/ini parses and rewrites INI files without losing the nested shape

> The npm/ini library turns INI text into a nested JavaScript object and writes it back, but sections become nested objects, globals need a section name on the way out, and values carry no types. It suits engineers who must interoperate with existing INI files rather than own their config format from scratch.

**npm/ini** — An ini parser/serializer in JavaScript

- Repository: https://github.com/npm/ini
- Stars: 821 · Forks: 225
- Language: JavaScript
- License: ISC
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/npm-ini

## Section headers become nested objects, not flat keys

npm/ini exposes a small surface: parse and stringify, with decode and encode offered as aliases. The mapping from file to memory is the part worth understanding before you adopt it. The API section states two rules plainly. Sections are treated as nested objects, and section-less items are treated as globals. In practice a key under a [database] header is reached as config.database.user, and a key that appears before any header stays on the root object.

Dotted headers nest further rather than flattening. A [paths.default] header produces config.paths.default, and the sample file in the usage notes puts a datadir value inside it. So the format on disk is flat, key equals value, while the in-memory representation is a tree, and the dot in the header is what decides how deep the tree goes.

This has one consequence for anyone writing a config editor. A file can hold two unrelated keys with the same name under different headers without collision, because they land on different objects. A file with no headers at all produces a flat object with no section metadata, which the writer cannot recover later without help from you.

## Repeated keys round-trip as bracketed array entries

The sample INI file shows how arrays are expressed on disk. It lists the same key three times, once per value, each written with an empty pair of square brackets appended to the name:

```ini
[paths.default]
datadir = /var/lib/data
array[] = first value
array[] = second value
array[] = third value
```

The bracketedArray option controls the writer. It is documented as true by default, and the stated reason is that some parsers treat duplicate names by themselves as arrays. When you push a fourth value onto the JavaScript array and write the file, the output contains four lines that each end in the bracket suffix.

That choice is about compatibility with other parsers rather than about this library alone. The read path groups the repeated bracketed keys back into an array, so the value survives the trip, and the order of the entries is the order they appear in the file. If a tool you care about expects bare duplicate keys instead, set bracketedArray to false and the suffix disappears from the output.

## Writing globals back requires a section name you choose

Global items have no header of their own, so the writer needs a label for them. The section option is that label. It is described as the identifier to use for global items and to prepend to all other sections. Every section in the output file therefore carries the chosen prefix.

The worked example in the usage notes makes the effect clear. The input file opens with a global scope key and then carries [database] and [paths.default] headers. The output file opens with a [section] header holding the global value, followed by [section.database] and [section.paths.default]. The nested paths survive; the top level gets wrapped in a name you supplied.

For backwards compatibility, any string passed as the options parameter is treated as the section option, so a short call still sets the same thing. This is the sharpest edge in the library. Reading a file and writing it back does not reproduce the original layout. It reproduces the data under a prefix, so a read-modify-write cycle in a tool that expects flat global keys will produce a file the other tool may not read the way you intended.

## Seven options decide the exact bytes on disk

The output format is controllable through a documented set of options, and the defaults are conservative:

```js
    whitespace : false ,
    align : false ,
    sort : false ,
    newline : false ,
    bracketedArray : true
```

Spacing around the equals sign is off by default, and the stated reason is better compatibility with old picky parsers. Aligning the equals character per section is off, and enabling it also turns the whitespace option on. Alphabetical sorting of sections and their keys is off, so the writer keeps the order of the object you hand it. A newline after each section header is off, a format the documentation says the TOSHIBA and FlashAir parsers require. Line endings follow the current platform by default, with win32 mapped to CR+LF and everything else to LF, and the platform option overrides that. The seventh option, section, was covered earlier.

Most of these default to off, so a plain stringify call produces compact output with no alignment, no sorting and no header spacing.

## Installing the package and running a first read and write

The documentation does not give an install command, so take the package from the npm registry under the name ini. The manifest declares version 7.0.0, points its main entry at lib/ini.js and lists bin/ and lib/ as the published files. It also sets a Node floor:

```json
  "engines": {
    "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
  }
```

The first real use is a read, a parse and a write. Import both halves of the library along with the file system helpers:

```js
import { writeFile , readFile } from 'node:fs/promises'
import { stringify , parse } from 'ini'
```

Parse the text into an object, then change what you need. The example assigns a global, changes a nested value and appends to an array:

```js
const config = parse(text)
config.scope = 'local'
config.database.database = 'use_another_database'
config.paths.default.array.push('fourth value')
```

Encode it back with the section option set, and the resulting file looks like this:

```ini
[section]
scope=local
[section.database]
user=dbuser
password=dbpassword
database=use_another_database
[section.paths.default]
tmpdir=/tmp
array[]=first value
array[]=second value
array[]=third value
array[]=fourth value
```

The old single-string form sets the same option in one argument:

```js
stringify(object,'section')
```

The API section also lists un and escape helpers that turn a string into a value or key safe to place in an INI file.

## Comments, key order and types do not survive the round trip

The limitation to plan around is that the library is a data converter, not a document editor. The sample file opens with a comment line marked as ignored, and the parser has no representation for it, so nothing in the returned object carries it and nothing in the writer can put it back. Key order follows the object you built unless you set sort, so a file that grouped keys for human readers loses that grouping on rewrite. There is also no typing: every value that comes out of the parser is a string, and a numeric port or a boolean flag has to be converted by your own code.

Two narrower cases deserve a mention. The dialect is INI only, with no support for a richer format, and the section prefix applies to every section in the output, so a file with a mix of global keys and nested headers cannot be written back in its original shape without splitting the write into two calls.

If your configuration needs comments, meaningful key grouping or typed values, ini is the wrong tool, no matter how convenient the read and write helpers are.

## INI versus JSON for the same configuration

The realistic alternative is not another INI library but a different format. JSON handled with the built-in parser and stringifier works on a native data model: numbers stay numbers, booleans stay booleans, null exists, and objects nest without any header indirection. Writing it needs no section name, and parsing is strict, so malformed input is reported rather than quietly accepted. JSON has no comment syntax and no support for repeated keys.

The ini approach inverts those trade-offs. It exists to read and write the flat key equals value files that many command line tools and embedded devices already use, including files with duplicate names, uneven spacing and quirky comment conventions. Its options exist to satisfy those older picky parsers, which is why whitespace is off by default and why the newline and platform switches are there at all.

So the decision is about ownership. If you own the config file end to end and can choose the format, a native one gives you types and no wrapper section. If the file already exists and must stay readable by something you do not control, ini fits it better than JSON does.

## Three major releases and a rising Node floor

The repository is not archived, and the last push was on 2026-07-02. Releases are tracked through a manifest that the file itself marks as partially managed by a template tool, with a release-please configuration and a CHANGELOG.md at the repository root.

The version history shows three majors: v5.0.0 on 2024-09-04, v6.0.0 on 2025-10-22 and v7.0.0 on 2026-05-08. That is one major roughly every year, so an adoption decision should assume you will eventually cross a major boundary and read the changelog for it.

The licence is ISC, a short permissive licence, so embedding the package in a product you distribute carries few obligations, though the licence text itself governs and is not legal advice. The upgrade cost that bites first is the runtime rather than the code: the current manifest accepts only Node ^22.22.2, ^24.15.0 or >=26.0.0, so a service pinned to an older Node line cannot install this release without moving its runtime first.

## Conclusion

Adopt ini when you must read and rewrite INI files that other tools already produce, and verify first that your target files contain no comment, key order or type information you need to survive the round trip. Do not adopt it for configuration you own end to end, where JSON or a typed format gives you real values and a strict parser without a section wrapper to invent. If you upgrade, check the Node floor first, because the current release declares support only for recent Node lines.

## FAQ

### How do I install the ini package for Node?

The documentation gives no install command, so take the package from the npm registry under the name ini. The manifest declares version 7.0.0, points its main entry at lib/ini.js and accepts only Node ^22.22.2 || ^24.15.0 || >=26.0.0.

### How does ini represent sections and arrays in JavaScript?

Section headers become nested objects, so a [paths.default] header is reached as config.paths.default, while keys that appear before any header stay on the root object as globals. Repeated entries are collected into a JavaScript array and written back with the [] suffix appended to each key.

### Does ini preserve comments and value types when it rewrites a file?

No. The parse step returns a plain data object in which the sample comment is ignored, and the writer encodes that object only, so comments do not come back. Every parsed value is a string, so numbers and booleans need your own conversion.

### Which options change the exact output of ini's stringify function?

The documented options are whitespace, align, sort, newline, platform, section and bracketedArray. They control spacing around the equals sign, alignment of the equals character, alphabetical ordering, a newline after each header, line endings, the wrapper section name and the [] suffix on array keys.

## Sources

- [Issues](https://github.com/npm/ini/issues)
- [License: ISC](https://github.com/npm/ini/blob/main/LICENSE)
- [npm/ini on GitHub](https://github.com/npm/ini)
- [README](https://github.com/npm/ini/blob/main/README.md)
- [Releases](https://github.com/npm/ini/releases)

---

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