Library / SDK
broofa/mime avatar
broofa/mime

broofa/mime: a generated type table, null on a lookup miss, and a readme that runs in CI

Mime types for JavaScript

2,357 stars320 forksTypeScriptMIT

At a glance

What is it?
The mime package for JavaScript is a small lookup library with no runtime dependencies, because the type table is a build artefact rather than a dependency, and it comes in two sizes with a stripped one behind a second export path. Its most unusual property is that its documentation is executed during the build, so the examples in the readme cannot drift from the code.
Who is it for?
mime is the right dependency for a server that has to answer what content type a file is, and it earns that position on three properties rather than on size. Lookups return null instead of guessing, which is what you want on a path that came from a request.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 67 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The type table is a build artefact, which is why there are no dependencies

The readme's headline promises three things, and the most interesting one is the middle claim, a compact package with no dependencies. It is one install command and two method calls:

bash
npm install mime
javascript
import mime from 'mime';

mime.getType('txt');
mime.getExtension('text/plain');

In a JavaScript ecosystem where a MIME lookup has historically been a package with a dependency on a package with a data file, that promise needs explaining, and the manifest explains it. The type database that the ecosystem treats as canonical appears among the development dependencies, along with its type definitions, and there is a build script whose only job is to produce the shipped type files. So the sequence is: the upstream database is read at build time, a script generates the type tables, and those generated tables are compiled into the package. Nothing is fetched at runtime because everything was fetched before publication. That is the whole design in three lines, and it is the right trade for a lookup table that every request might touch. The export map confirms the shape, with four subpaths: the main entry, a lite entry, and the two generated type tables exposed directly so that a caller can compose their own instance. The files array is also unusual in a good way, listing the source directory alongside the built output, which means a bundler that prefers to compile from source can, and which makes a source map meaningful for consumers. The two alternative libraries in the same ecosystem appear among the development dependencies too, alongside the scoring library used to choose a preferred extension for a type, and the most plausible reason for that is that the test suite compares this implementation's answers against theirs. A lookup table's correctness is entirely a question of agreement with the reference data, so testing against the other implementations is a genuinely good use of a dependency.

The readme is generated, and its examples are executed rather than typed

The first thing in the readme file is a comment saying it is auto-generated from another file in the source directory and that changes should be made there. That in itself is unremarkable. What makes it worth a section is the evidence that the generator runs the code. Look at the example blocks. They are tagged with the language and, in one case, with an extra flag. The results are written with a marker and an arrow character rather than with a comment, which is the convention of a tool that captures output rather than the convention of a human writing an expected value. And that tool is in the development dependencies, alongside a library whose purpose is running the code blocks in a markdown file. So the readme is a test suite that happens to be documentation: each example is executed and its output compared with what is written. That is a stronger guarantee than any review, and it is the reason you can trust the specific outputs quoted below without taking the author's word for them. The build chain around it is equally tight, and it is worth reading as a sequence because each step closes a hole. The pretest hook builds the project, so the tests can never run against a stale build. The prepublish hook builds again, so a published tarball is never made from an old directory. The prepublish-only hook runs the tests, so a publish cannot proceed on a red suite. The test command itself runs the platform's own test runner and then a shell script that checks the export map, which is the part most packages forget and the part most likely to break silently, since a missing subpath export is not an error until somebody imports it.

Lookups return null, and that contract is the API

The three query methods have a contract worth stating precisely, because it is the thing you will design your code around. Given a path or an extension, the type lookup returns the type string, and it handles more shapes than you would expect. A bare extension works. A path with forward slashes works. A path with backslashes works, which is the detail that matters on Windows and which a naive implementation gets wrong. A name with several dots takes the last one, and a name that begins with a dot and then has a real extension resolves to the real extension rather than treating the whole thing as an extension. And when nothing matches, the answer is null. Two examples make the boundary concrete. A path-shaped string whose last segment has no dot returns null, and an unrecognised token returns null.

javascript
mime.getType('foo/txt');
mime.getType('bogus_type');

So the function never throws, never guesses and never invents a default type, which is exactly right for a function you call on a path that came out of a request. Contrast that with the mutating method in the same library, which throws when you try to map a type onto an extension that is already taken unless you pass a flag to override. The asymmetry is the design: a query about the world cannot fail loudly because the world's answer might be nothing, while a change to your own configuration conflicting with itself is a programming error and should stop you. Two more details in the query surface. The reverse lookup ignores charset parameters, so a content type header taken straight from a request, complete with its character set, resolves to the right extension. And the fourth version added a method that returns every extension for a type rather than one, as a set, so an image type comes back with all three of its aliases instead of only the preferred one.

Immutable by default, with the table exposed as data you can recombine

The default objects exported by the package are immutable, and that is a stronger statement than it first sounds. An immutable lookup table cannot be altered by any code that loaded the library earlier in the process, which in a server means one plugin cannot change what another plugin sees when it maps a filename. The readme is explicit that mutability is available but only on instances you create, and it shows how. You construct a custom instance from one or more type maps, and the maps are imported from the two subpaths the package exports. Each argument is handed to the definition method, and the readme notes the useful equivalence that passing two maps in the constructor is the same as constructing an empty instance and defining each map in turn. So there are two layers, a mechanism and a database, and the database is published as data. That composition is the part that makes the package flexible without making it complicated. You can take the full table, the standard table alone, the other table alone, or a table of your own, and combine them. The definition method itself has a clear rule: mapping a type to an extension that is already defined throws unless you pass a flag to force it, which is the same query-versus-command asymmetry as before, applied to configuration. Forgetting to define a type you expected is a common and confusing failure, and forcing the mapping is how you deal with a genuine collision, so having it be an explicit, visible act rather than a silent last-write-wins is the right call.

Two sizes, split on a line the ecosystem already draws

The package ships in two sizes and the split between them is drawn on a line that means something specific. The full build is described as covering more than eight hundred types and more than a thousand extensions. The lite build is a drop-in replacement with the unofficial types removed, and the readme names exactly which ones those are: the personal registration prefix, the experimental prefix, and the vendor prefix. Read that list and you can see the reasoning. Official types are the ones every browser and tool agrees on. The other three prefixes are where personal experiments, experimental proposals and vendor-specific formats accumulate, and the volume of them is what makes a lookup table heavy. For a server that sets a content type on a file it is about to send, the official set is almost always sufficient, and shipping the rest is dead weight in every bundle. For a tool that has to identify a file a user uploaded, which might be anything, the full table is the point. The lite build is a different import path rather than a configuration option, which means the choice is visible in your source and cannot be changed by a runtime flag from somewhere else, and both entry points have their own bundle size badge in the readme so the saving is quantified rather than asserted. The composition story from the previous section also applies here, because the generated tables are importable on their own, so a project that wants the official types plus three vendor exceptions can build exactly that instead of choosing between the two ends of the scale.

Migrating from version 3: two removals and two runtime requirements

The upgrade note at the top of the readme is the most practically useful paragraph for anyone still on the previous major version, and its wording is worth noticing. It says version 4 is API compatible with version 3, with one exception, and then the word one is struck through and replaced with two. Someone revised that count in place, which tells you the second removal was found after publication. The two removals are different in kind, and the difference is what determines how much work your upgrade is. The first is that direct imports of properties from the package are no longer supported, so code that reached in and pulled a property off the module object has to change shape and call a method instead. The second is that the definition method cannot be called on the default object, which is the immutability decision from the previous section arriving as a breaking change: if you were registering your own types at startup, you now need a custom instance. The first is a mechanical refactor across a handful of call sites. The second changes the architecture of your initialisation, because a mutable singleton has to become an object you pass around. Then there are the two runtime requirements, which are not breaking API changes but will still stop your build. The package is an ES module only, so a project that imports it from CommonJS has to change its own module system, and the readme links an explanation of that migration. The runtime must support a specific language generation, which for a modern Node release is not a constraint at all. And the type definitions are bundled, so the separately installed type package for the previous version is now redundant and should be removed rather than left in place.

A command line, a platform test runner, and a linter that is only a formatter

Two smaller things round out the picture, and both are about what the project has chosen not to carry. The first is the command line tool, which is declared in the manifest as a binary named after the package, and it does both directions. Given a path it prints the type; given a type, with a flag, it prints the extension.

bash
mime scripts/jquery.js
mime -r image/jpeg

The example the readme uses is a script file from the package's own scripts directory, which is a nice touch because the answer it produces is the one everybody has an opinion about, and the interesting part is that the current answer is the text and JavaScript type rather than the application one that half the internet still returns. For a data lookup library, having a tool that runs in a shell pipeline is worth more than it sounds, because a build script or a shell function can use it without importing anything. The second thing is the test and lint setup, and here there is a small inconsistency worth pointing out so you do not go looking for a linter that is not there. The repository contains an eslint configuration file, and eslint is not in the development dependencies, and the script named lint runs the formatter in check mode and nothing else. So the linting story is a formatter and no static analysis of the source, and the eslint file is a leftover from when the project used one. The tests themselves are in good shape: they run on the platform's built-in test runner rather than a framework, which means no test dependency and no runner configuration to outgrow, plus a shell script that verifies the export map. A contributor who adds an export path therefore has a test that fails until the map is updated.

Editorial conclusion

mime is the right dependency for a server that has to answer what content type a file is, and it earns that position on three properties rather than on size. Lookups return null instead of guessing, which is what you want on a path that came from a request. The default objects are immutable, so a lookup table cannot be changed by a library that happens to be loaded earlier in the process. And the two sizes mean a browser bundle does not have to carry the vendor types nobody serves. Two things to check before you adopt it. Which entry point you want, since the stripped build is a different import path rather than a configuration option, and whether your runtime is an ES module, because version 4 dropped support for the other style and that is the change most likely to break an existing build. Then decide on the version 3 migration deliberately: the upgrade notes name two removed capabilities and two runtime requirements, and the two removals are different in kind, one being how you reach the API and the other being whether you are allowed to change it at all.

Frequently asked questions

How does the package have no runtime dependencies?

The canonical MIME database is a development dependency and a build script generates the shipped type tables from it, so the data is compiled into the package rather than fetched at runtime. The generated tables are also exposed as export subpaths, so a caller can import them and build a custom instance.

What does the lookup return when it cannot identify a file?

Null, not an exception and not a guess. The readme shows a path whose last segment has no extension and an unrecognised token both returning null, and it handles paths with either slash style plus names with several dots, taking the last extension.

How do I add my own MIME types?

The default exported object is immutable, so you construct a custom instance from one or more type maps, which the readme notes is equivalent to defining each map in turn. The definition method throws if a type is mapped to an extension that is already taken unless you pass a flag to force it.

What breaks when I move from version 3 to version 4?

Two things were removed, direct imports of properties from the package and calling the definition method on the default object. There are also two runtime requirements: the package is an ES module only, so a CommonJS project must change its own module system, and the bundled type definitions make the separately installed type package redundant.

What is the difference between the full build and the lite build?

The lite build is a drop-in replacement with the unofficial types stripped out, specifically the personal registration, experimental and vendor prefixes, and it is a separate import path rather than a configuration option. The full build covers more than eight hundred types and more than a thousand extensions.

Official sources

  1. broofa/mime on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/broofa-mime.svg)](https://hysenlabs.com/projects/broofa-mime)