DEV Community

Elizabeth Sobiya
Elizabeth Sobiya

Posted on

I couldn't find an npm package to sort filenames into buckets, so I built one

Last week I was building a file-management feature. Users could upload
documents, and I needed to sort them into sections. One bucket for
NOCs, one for agreements, one for invoices, one for anything else.

Simple enough. I opened npm expecting to find a package for it.

What I found instead

Every search under "sort files" or "classify filename" returned one of
three things:

  • sort-package-json variants (sorting keys inside package.json)
  • Natural-sort utilities (alphabetical ordering of file lists)
  • File-type detectors (identifying MIME types from extensions)

Nothing that took a list of filenames and a list of rules and gave
back named buckets.

I asked around. A few devs suggested "just use .filter()". Fair, for
one bucket. But once you need:

  • Multiple buckets with priority order
  • A fallback bucket for unmatched files
  • Natural sort inside each bucket
  • Rules from a config file or user input

...you're rewriting the same 40 lines in every project. That's
package-shaped.

So I built classify-filename

import { classify } from 'classify-filename';

const files = [
  'NOC_2024.pdf',
  'noc_letter.pdf',
  'agreement_v1.docx',
  'invoice_2.pdf',
  'invoice_10.pdf',
  'random.txt',
];

const result = classify(files, [
  { name: 'noc',        match: /^noc/i },
  { name: 'agreements', match: 'agreement' },
  { name: 'invoices',   match: /^invoice/i },
]);
Enter fullscreen mode Exit fullscreen mode

The result:

{
  sections: {
    noc:           ['noc_letter.pdf', 'NOC_2024.pdf'],
    agreements:    ['agreement_v1.docx'],
    invoices:      ['invoice_2.pdf', 'invoice_10.pdf'],
    uncategorized: ['random.txt'],
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice invoice_2.pdf comes before invoice_10.pdf. That's natural
sort, the default behavior, because alphabetical sort would put 10
before 2 and that's usually not what you want with filenames.

Design decisions I actually thought about

A few choices that felt small but mattered.

Section order is priority order.
If a file matches two sections, the earlier one wins. No priority
numbers, no most-specific-match algorithm. Array order is the mental
model users already have when they write a list of rules.

Case-insensitive by default.
Files are messy. NOC_2024.pdf and noc_letter.pdf should land in
the same bucket without the user thinking about it. Regex matchers
still respect their own flags. If you write /^NOC/ you probably
meant it.

A fallback bucket, not a silent drop.
Unmatched files go to uncategorized by default. Silent drops cause
the "where did my files go?" bug I've written before. You can opt into
the drop by passing fallback: false.

An explain mode for debugging.
Set explain: true and the result includes a filename to section map,
so when someone asks "why did noc_agreement.pdf end up in NOC and
not agreements?", you can see the answer in one glance.

What's not in v1 (on purpose)

  • No glob support. Regex covers it. Adding globs invites ambiguity with substring matches unless done as an explicit wrapper. Planned for v1.1.
  • No file-metadata rules. Filename only for now. Size, date, and extension helpers coming in v1.2.
  • No CLI. This is a library. If people want to organize a folder from the command line, that's v1.3.

Each of these could have gone into v1. I left them out because the
scope creep would have delayed shipping, and the API stays cleaner if
each feature earns its way in with real user demand.

The stack

  • TypeScript, dual CJS + ESM output via tsup
  • Zero runtime dependencies
  • Tested with Vitest, around 15 tests covering the matcher and classifier
  • ~2 KB minified

Try it

npm install classify-filename
Enter fullscreen mode Exit fullscreen mode

One thing I'd genuinely like feedback on: section-array-order as
priority. Does that feel natural to you, or would you rather see an
explicit priority: number field?

Would love to hear how you'd approach this. If you find it useful, a
star on the repo helps me know it's worth pushing v1.1 out.

Top comments (0)