As well as an API, Web Listener provides a convenience CLI which can be used for local development, or even in production if you have simple needs.
This tool supports:
- local file serving (including single-page-app support and Content-Encoding negotiation)
- additional headers
- proxying to another server
- serving static fixtures
- static redirects
- basic templating for fixtures and redirects
- running multiple servers simultaneously
- running background tasks
- registering custom Javascript handlers
- loading config and serving files directly from zips
- combining multiple configuration files
Simple servers can be configured via CLI flags. Complex servers can be configured via JSON.
For full documentation, view the manual page with:
npx web-listener --helpnpx web-listener . --port 8080Serves files from the current working directory (.) on port 8080, bound to localhost (i.e.
not available remotely). Dotfiles and tilde-files are hidden by default (except .well-known),
and the server is protected against directory traversal attacks. index.htm and index.html are
automatically recognised as index files for directories.
The server can be stopped by sending SIGINT (Ctrl+C in most terminals).
For a full list of available commandline flags, run npx web-listener --help.
You can use JSON configuration when you need more advanced features or more control over the server. This can be provided inline to the command, or in an external file.
npx web-listener --config-file ./config.jsonExample config.json (equivalent to web-listener . --spa index.html --port 8080):
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "files",
"path": "/",
"dir": ".",
"options": { "fallback": { "filePath": "index.html" } }
}
]
}
]
}Paths in config files are relative to the file, not the current working directory.
When using an external configuration file, you can reload the config without restarting the process by sending SIGHUP, or providing a newline to stdin (i.e. pressing return in the terminal).
A JSON schema is available which can be used for validation of JSON configuration, and to provide editor assistance in compatible IDEs:
{
"$schema": "./node_modules/web-listener/schema.json",
"servers": [{ "port": 8080, "mount": [] }]
}npx web-listener --config-json '{"servers":[{"port":8080,"mount":[{"type":"files","path":"/","dir":".","options":{"fallback":{"filePath":"index.html"}}}]}]}'Paths in inline JSON are relative to the current working directory.
Some features (fixtures and redirects) support very basic templates:
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "redirect",
"path": "/*thepath.php",
"target": "/${thepath}${?}"
}
]
}
]
}This example redirects requests for .php files to remove the extension but preserve all query
parameters.
Templates behave like Javascript templates, using the ${variable} syntax. The available variables
are:
- path parameter names (as shown in the example above:
${thepath}), - named query / search parameters (with
${?parameter}), - the entire query / search part of the request (with
${?}).
Templates are mostly useful for creating dynamic redirects as shown above, but can also be used in
the body and header values for fixture definitions.
To specify a fallback value, use the shell-style :-:
${pathParameter:-fallback}
Redirects automatically uri-encode parameters as needed for convenience (but you can override this
by explicitly specifying raw or uri encoding). Other templates do not apply any encoding by
default. To apply encoding, specify the type you need:
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "fixture",
"method": "GET",
"path": "/*path.htm",
"status": 200,
"body": "<html><body><h1>You requested ${html(path)}!</h1><p>You also added: ${html(?):-<em>no query string</em>}</p></body></html>"
},
{
"type": "fixture",
"method": "GET",
"path": "/*path.json",
"status": 200,
"body": "{\"path\":${json(path)},\"page\":${int(?page:-1)}}"
}
]
}
]
}
The available encoding types are:
raw(): the original value, unmodified (path and query parameters will be URL decoded)html(): the value with special HTML characters escaped using&escapesjson(): the value as a JSON string (even if it could be represented as a number, it will be encoded as a string)int(): the value as a plain integer, safe to include in HTML, JSON, URIs, etc. (prints0if the input is not a valid integer)uri(): the value mapped throughencodeURIComponent(for multi-component path parameters, each component is encoded individually then joined with/). This is the default encoding when writing redirects, except for${?}which is encodedrawby default.
Note that this template language is designed to serve common needs for simple templating, such as redirects and simple testing stubs; it is not intended to be comprehensive. If you have more demanding needs you should use a dedicated templating library and build an application, rather than configuring endpoints using the CLI.
Serve static files, and reply to unknown paths with index.html's content:
npx web-listener . --spa index.html --port 8080{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "files",
"path": "/",
"dir": ".",
"options": { "fallback": { "filePath": "index.html" } }
}
]
}
]
}Serves files with the Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers (which
allow access to various high-performance Javascript APIs):
npx web-listener . -H 'Cross-Origin-Opener-Policy: same-origin' -H 'Cross-Origin-Embedder-Policy: require-corp' --port 8080{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "headers",
"headers": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp"
}
},
{
"type": "files",
"path": "/",
"dir": "."
}
]
}
]
}{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "proxy",
"path": "/api",
"target": "http://localhost:9000"
},
{
"type": "files",
"path": "/",
"dir": ".",
"options": { "fallback": { "filePath": "index.html" } }
}
]
}
]
}{
"servers": [
{
"port": 8080,
"mount": [{ "type": "files", "path": "/", "dir": "./web1" }]
},
{
"port": 8081,
"mount": [{ "type": "files", "path": "/", "dir": "./web2" }]
}
]
}Launch background tasks. This example shows how to run the TypeScript compiler in watch mode while serving files.
Note: if web-listener is stopped while the process is still running, it will be sent a SIGKILL
signal.
npx web-listener . --exec 'tsc -w'{
"servers": [
{
"port": 8080,
"mount": [{ "type": "files", "path": "/", "dir": "." }]
}
],
"backgroundTasks": [{ "command": "tsc", "arguments": ["-w"] }]
}{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "fixture",
"method": "GET",
"path": "/config.json",
"status": 200,
"body": "{\"env\":\"local\"}"
},
{
"type": "fixture",
"method": "GET",
"path": "/robots.txt",
"status": 200,
"body": "User-agent: *\nDisallow: /"
}
]
}
]
}Note: it is possible to use Templates in fixture responses.
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "redirect",
"path": "/old-things/:id",
"target": "/new-things/${id}"
}
]
}
]
}Note: it is possible to use Templates in redirect targets.
Various common web file extensions are recognised by default and associated with the correct mime type, but you can add or replace extension-to-mime mappings if needed.
npx web-listener . --mime 'foo=application/foo'{
"servers": [{ "port": 8080, "mount": [{ "type": "files" }] }],
"mime": ["foo=application/foo"]
}Or:
{
"servers": [{ "port": 8080, "mount": [{ "type": "files" }] }],
"mime": { "foo": "application/foo" }
}You can also reference a mime type mapping file in Apache .types format
npx web-listener . --mime-types ./mime.types{
"servers": [{ "port": 8080, "mount": [{ "type": "files" }] }],
"mime": ["file://mime.types"]
}Online compression of files is not supported, but if you have pre-compressed copies of the files available in the filesystem, you can enable Content-Encoding negotiation.
The following examples enable serving *.br files as brotli-encoded, and *.gz files as gzip.
Note that you can also generate these pre-compressed files automatically at startup by adding
--write-compressed to the CLI flags. This can be combined with --no-serve to only compress the
files and not actually start a server (e.g. if you want to generate pre-compressed files as part of
a build process).
npx web-listener . --brotli --gzip{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "files",
"options": {
"negotiation": [
{
"feature": "encoding",
"options": [
{ "value": "br", "file": "{file}.br" },
{ "value": "gzip", "file": "{file}.gz" }
]
}
]
}
}
]
}
]
}For advanced needs you can import an arbitrary Javascript module and map it to a path.
This is intended for simple, stateless routes. Though it is possible to use it for more complicated
scenarios, using web-listener as a library and building your own server is recommended in such
cases, since that gives much greater control over how your code runs.
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "custom",
"method": "GET",
"path": "/",
"import": "./my-handler.mjs"
}
]
}
]
}export default (req, res) => {
res.end('a message from my custom handler');
};Notes:
-
if you
import 'web-listener'in your handler script, it will automatically be deduplicated with the copy powering the CLI tool. This avoids issues with duplicate classes or high memory usage, but means you may get an unexpected version if you are using an old version of the CLI. -
a
Routeris also a handler, so you can export aRouterand have access to the full power of theweb-listenerlibrary. When doing this, it is best to leavemethodasnull(the default) so that your handler can act on all request types and sub-paths. -
due to V8's module cache, it is not possible to reload a Javascript file after it has been loaded. This means you will not see changes until you fully stop and re-run the
web-listenercommand. It is possible to add versioning strings to imported paths (e.g../my-handler.mjs?v1) to work around this, but doing so will lead to ever-increasing memory usage as old scripts remain in memory, and is not suitable for production deployments or long-lived development servers.
All configuration files, custom handler scripts, and served directories can be specified as paths within a zip archive, using the form:
/path/to/file.zip/path/within/zip
If a config file is in a zip archive, any paths it contains will be relative to its location in the
zip. This can be used to produce single-file "bundles" for simple services. For example, a zip named
bundle.zip containing:
-
config.json:{ "servers": [ { "port": 8080, "mount": [ { "type": "custom", "path": "/custom", "import": "./custom-handler.mjs" }, { "type": "files", "path": "/", "dir": "static" } ] } ], "mime": ["file://apache.types"] } -
staticindex.html
Could be loaded with:
npx web-listener --config-file ./bundle.zip/config.jsonWhen performing path resolution, the zip is interpreted as a regular directory, so config files
inside zip archives can still reference files outside the zip by navigating to them using .., or
using absolute paths.
Note that as with all CLI configuration, you should not use untrusted bundles, as they have the ability to access files and execute arbitrary code using the permissions of the current user.
The recommended way to generate a zip archive for this purpose on MacOS and Unix is:
zip -8 -X -r -n .br:.gz:.zstd:.deflate:.png:.jpg:.jpeg bundle.zip config.json static [...]The flags:
-8sets maximum compression (but not-9, which would cause it to ignore the-nflag);-Xskips additional file metadata such as user and group ID;-renables recursive scanning of files;-n .br:.gz:...disables compression for specific filetypes which are unlikely to benefit from it (and storing these uncompressed means they can be served directly from the file, without needing to decompress them at runtime).
If you want to break up your configuration into multiple files, or run multiple distinct services
(e.g. a production server running multiple simple applications in one process to minimise memory
requirements), you can use the delegate mount type:
{
"servers": [
{
"port": 8080,
"mount": [
{
"type": "delegate",
"path": "/thing-1",
"config": {
"file": "./other-config.json",
"serverPort": 2000
}
}
]
}
]
}{
"servers": [
{
"port": 2000,
"mount": [
{
"type": "files",
"path": "/",
"dir": ".",
"options": { "fallback": { "filePath": "index.html" } }
}
]
}
]
}By default, mime types defined in the referenced file will be merged with the
root config's mime types, and background tasks will be ignored. You can
change these behaviours by setting includeMime and includeBackgroundTasks in the "delegate"
configuration.
If the referenced config sets options on the server, these are ignored. See below for an approach which lets each file define its own server.
An alternative way to combine configuration is to let each config file define its own server:
{
"servers": [
{
"file": "./other-config.json",
"serverPort": 2000
}
]
}(serverPort can be omitted to load all servers from the referenced file at once)
{
"servers": [
{
"port": 2000,
"mount": [
{
"type": "files",
"path": "/",
"dir": ".",
"options": { "fallback": { "filePath": "index.html" } }
}
]
}
]
}This allows the referenced config to set options on the server.