CLI tool
unjs/listhen avatar
unjs/listhen

listhen, the HTTP listener that prints a QR code and shuts down cleanly

👂 Elegant HTTP Listener

593 stars46 forksTypeScriptNOASSERTION

At a glance

What is it?
listhen is a small TypeScript library that starts an HTTP server for a handler or an h3 app, prints the URL, shows a QR code, and closes the server on exit. It suits anyone who needs a dev server or a one-off listener in a Node project without hand-writing port selection, TLS, watch mode and shutdown handling, and its option surface is small enough to read in one sitting.
Who is it for?
Adopt listhen if you want a dev server or a short-lived HTTP listener inside a Node or TypeScript project and would rather not assemble port selection, watch mode, TLS and shutdown handling yourself, since the package does all of that and prints a QR code for testing on a phone. Do not adopt it if you need a production server, because several of its defaults are explicitly forced off in production, including opening the browser and copying to the clipboard.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 4 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 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A listener, not a framework

listhen describes itself as an elegant HTTP listener, and the API surface reflects that. There are two entry points, listen and listenAndWatch, and you hand the first one a request handler:

ts
const handler = (req, res) => {
  res.end("Hi!");
};

// listener: { url, getURL, server, close, ... }
const listener = await listen(handler, options);

The returned listener object exposes the url, a getURL function, the underlying server and a close method, and the comment in the README lists those as the non-exhaustive set of what you get back.

The point of the library is not the server. Node's http module and every framework above it can serve requests. The point is everything around the server that a real project needs and nobody wants to write twice: picking a free port, printing the address, deciding whether to bind to all interfaces, handling TLS, watching files for changes, and shutting down cleanly so a test run or a CI job does not hang on an open socket.

The library sits in the unjs family alongside h3, jiti, uqr, untun, get-port-please, crossws, consola and citty, and it composes with them rather than reimplementing them.

Running it from the command line with npx

The quick usage path is a single command. You write a handler, in this case index.ts:

ts
export default (req, res) => {
  res.end("Hello World!");
};

or an h3 app, if you prefer that shape:

ts
import { createApp, eventHandler } from "h3";

export const app = createApp();

app.use(
  "/",
  eventHandler(() => "Hello world!"),
);

Then you invoke the listener with npx, and the -w flag is what turns on watch mode with TypeScript support:

sh
npx listhen -w ./index.ts

TypeScript support comes from unjs/jiti, the on-the-fly TypeScript and ESM loader that the unjs projects use broadly. That is what makes the -w form work: there is no compile step and no separate dev server configuration, because the file is loaded at runtime and reloaded when it changes.

The package.json exposes two binaries, listen and listhen, both pointing at ./bin/listhen.mjs, so either name works. The published entry points follow the same pattern, with the root export providing listen and listenAndWatch and a separate ./cli export for the command line pieces, with ESM and CommonJS builds for each.

The port and hostname defaults decide who can reach your server

Two defaults deserve attention before you run this on a shared machine or a network.

The port default is process.env.PORT, or 3000, or a memorized random port from get-port-please. The memorization matters: if you have run the same handler before, you get the same port again, which is convenient for a demo and convenient for a test that assumes stability.

The hostname default is process.env.HOST or 0.0.0.0. That is a wildcard bind, and combined with the public option it determines whether the server is reachable from outside the machine.

The public option defaults to false for development or when hostname is localhost, and true for production. When enabled, listhen tries to listen on all network interfaces, and the same thing can be done with the --host CLI flag. So in production mode, by default, the server is exposed on the network, while in development it is not, unless you ask for it.

The showURL option, which prints the listening URL in the CLI, defaults to true and is force-disabled in a test environment. The open option, which opens the URL in a browser, and the clipboard option, which copies the URL to the clipboard, both default to false and are force-disabled on test and production environments. That is the sensible design: the browser-opening and clipboard features are conveniences for a developer at a desk, and the library declines to trigger them when it detects a production environment.

HTTPS with self-signed certificates, and the environment detection behind it

HTTPS is a boolean or an object, defaulting to false, and setting it to true makes listhen use an auto-generated self-signed certificate.

For more control, the object form takes a domains array, defaulting to localhost, 127.0.0.1 and ::1, and a validityDays number, defaulting to 1. You can also point at your own files with https: { cert, key }, where the values are paths to the SSL certificates, adding passphrase if the private key is encrypted. A keystore is handled with https: { pfx } and its path, again with passphrase when it is password protected. Inline certificates and keys are also possible, in which case they should start with --.

The default validity period of one day is the detail that shows the intent. A self-signed certificate with a one day lifetime is not meant to be trusted by anything, it exists so that a browser will let you through a TLS warning while you are developing. That is a reasonable default for a dev tool and a poor default for anything else, which is why the option lets you supply your own certificate.

Underneath all of this sits the environment detection. The isTest option defaults to process.env.NODE_ENV === 'test', and it is what forces showURL, open and clipboard off in the environments where they should not fire.

WebSockets, extra URLs and the QR code

The WebSocket support is labelled experimental in the README, and it is opt-in with the ws option, defaulting to false. The option can be a function acting as a Node.js upgrade handler with the signature (req, head) => void, or an object using CrossWS Hooks. When you use the dev server CLI you can pass --ws and define a named export called websocket to get CrossWS Hooks with HMR support.

The CrossWS package is what makes zero configuration WebSockets possible, and it is a dependency of the library rather than something you configure yourself.

Then there are the output conveniences. The qr option defaults to true and prints a QR code for the public address, generated by uqr. The publicURL option sets which URL is shown in the CLI output, defaulting to the first public URL that is listening, and extraURLs lets you add more entries of the form { title, url?, env? }, where url is a static link and env resolves the URL from an environment variable at showURL() time.

The default extraURLs is a single entry for Portless, resolved from the PORTLESS_URL environment variable, and passing extraURLs: [] disables the feature. The Portless default tells you something about the intended audience: listhen is built for the local development of full stack applications, where a public URL for the frontend and a separate URL for the API both need to be visible at the same time.

The URL can also be opened in a browser, with the open option, or copied to the clipboard, with the clipboard option, and both silently ignore errors.

Clean shutdown, a CLI binary, and a fast moving release line

The last feature in the list is the one that matters most in automated environments. The autoClose option defaults to true and closes the server automatically when an exit event, SIGTERM, SIGINT or SIGHUP signal is received in the process. The graceful shutdown itself comes from http-shutdown, which lets in-flight requests finish rather than dropping them.

Without that, a listener started inside a test or a CI step keeps the event loop alive and the job hangs until it is killed. With it, the process exits when the work is done, which is the difference between a test that finishes and a test that needs a timeout.

The tooling around the package is modern. The build uses unbuild, the tests use Vitest with coverage, the linting uses oxlint with oxfmt for formatting, and the release script chains pnpm test, pnpm build, changelogen, pnpm publish and a git push with tags. The README itself is generated with automd, which is why the options section and the badges look machine written, and the repository carries a renovate.json for dependency updates.

The release history shows regular work: 1.9.1 on 2026-04-01, 1.10.0 on 2026-04-30 and 1.10.1 on 2026-07-20, with the last push on 2026-10-18. The version in the package.json is 1.10.1 and the licence is MIT, with the README stating the package is published under the MIT license and made by pi0 and the community.

Editorial conclusion

Adopt listhen if you want a dev server or a short-lived HTTP listener inside a Node or TypeScript project and would rather not assemble port selection, watch mode, TLS and shutdown handling yourself, since the package does all of that and prints a QR code for testing on a phone. Do not adopt it if you need a production server, because several of its defaults are explicitly forced off in production, including opening the browser and copying to the clipboard. Before you depend on it, check the port and hostname defaults, since it binds to 0.0.0.0 and the public option decides whether it listens on all interfaces, decide whether the experimental WebSocket support is acceptable for your use, and note the release picture, where v1.10.1 shipped on 2026-07-20 and the last push was on 2026-10-18.

Frequently asked questions

How do I run an app with listhen from the command line?

Write a handler such as an index.ts file that exports a request handler, then run npx listhen -w ./index.ts. The -w flag enables watch mode, and TypeScript support comes from the jiti loader rather than a compile step, and the package also exposes listen and listhen as binary names.

What port and hostname does listhen listen on by default?

The port default is process.env.PORT, or 3000, or a memorized random port from get-port-please. The hostname default is process.env.HOST or 0.0.0.0, and the public option, false for development and true for production, decides whether listhen listens on all network interfaces.

How do I use HTTPS with listhen?

Set the https option to true for an auto-generated self-signed certificate, or to an object for custom options including domains, which defaults to localhost, 127.0.0.1 and ::1, and validityDays, which defaults to 1. You can also point at your own files with https: { cert, key } or https: { pfx }, adding passphrase for encrypted keys.

Does listhen support WebSockets?

Yes, as experimental support that is off by default. The ws option can be a function acting as a Node.js upgrade handler or an object using CrossWS Hooks, and the dev server CLI accepts a --ws flag along with a named export called websocket for HMR support.

How does listhen avoid hanging a test or CI job?

The autoClose option defaults to true and closes the server when an exit event, SIGTERM, SIGINT or SIGHUP signal is received, with graceful shutdown handled by http-shutdown so in-flight requests are not dropped.

Official sources

  1. Issues
  2. README
  3. Releases
  4. unjs/listhen on GitHub
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/unjs-listhen.svg)](https://hysenlabs.com/projects/unjs-listhen)