# Using CLI

> Use the srvx CLI command to easily start a development or production server.

You can run `srvx` with your preferred runtime without installation:

<CodeGroup>

```bash [npm]
npx srvx
```

```bash [pnpm]
pnpx srvx
```

```bash [yarn]
yarn dlx srvx
```

```bash [Deno]
deno -A npm:srvx
```

```bash [Bun]
bunx --bun srvx
```

</CodeGroup>

## Usage

```sh
srvx   - Universal Server.

SERVE MODE

# srvx serve [options]
$ srvx serve --entry ./server.ts    # Start development server
$ srvx serve --prod                 # Start production  server
$ srvx serve --port=8080            # Listen on port 8080
$ srvx serve --host=localhost       # Bind to localhost only
$ srvx serve --static=./dist        # Serve static files (no entry needed)
$ srvx serve --import=jiti/register # Enable [jiti](https://github.com/unjs/jiti) loader
$ srvx serve --tls --cert=cert.pem --key=key.pem  # Enable TLS (HTTPS/HTTP2)

FETCH MODE

# srvx fetch|curl [options] [url]
$ srvx fetch                  # Fetch from default entry
$ srvx fetch /api/users       # Fetch a specific URL/path
$ srvx fetch --entry ./server.ts /api/users # Fetch using a specific entry
$ srvx fetch -X POST /api/users # POST request
$ srvx fetch -H "Content-Type: application/json" /api # With headers
$ srvx fetch -d '{"name":"foo"}' /api # With request body
$ srvx fetch -v /api/users    # Verbose output (show headers)
$ echo '{"name":"foo"}' | srvx fetch -d @- /api # Body from stdin

COMMON OPTIONS

  --entry <file>            Server entry file to use
  --dir <dir>               Working directory for resolving entry file
  -h, --help                Show this help message
  --version                 Show server and runtime versions

SERVE OPTIONS

  -p, --port <port>         Port to listen on (default: 3000)
  --host, --hostname <host> Host to bind to (default: all interfaces)
  -s, --static <dir>        Serve static files from the specified directory (default: public)
  --prod                    Run in production mode (no watch, no debug)
  --import <loader>         ES module to preload
  --tls                     Enable TLS (HTTPS/HTTP2)
  --cert <file>             TLS certificate file
  --key  <file>             TLS private key file

FETCH OPTIONS

  -X, --method <method>     HTTP method (default: GET, or POST if body is provided; --request is a curl alias)
  -H, --header <header>     Add header (format: "Name: Value", can be used multiple times)
  -d, --data <data>         Request body (use @- for stdin, @file for file)
  --host <host>             Host for a schemeless URL/path (default: localhost)
  --tls                     Use https for a schemeless URL/path
  -v, --verbose             Show request and response headers

  Exits with code 22 on a non-2xx response (like curl --fail).

ENVIRONMENT

  PORT                      Default port to listen on
  HOST                      Default host to bind to
  NODE_ENV                  Set to production for production mode.
```

## Port and host precedence

The port and host are resolved with the following precedence (highest first):

<steps level="4">

#### CLI flag (`--port` / `--host` / `--hostname`)

#### Module option (`port` / `hostname` exported from your server entry)

#### Environment variable (`PORT` / `HOST`)

#### Default (`3000` / all interfaces)

</steps>

## Exit codes

In **fetch mode**, `srvx fetch` exits with code **22** for any non-2xx response, and `0` for a 2xx response.

## Runtime notes

<note>

The `--import` flag preloads an ES module (e.g. a loader like `jiti/register`). It is applied on **Node.js and Bun only** — on **Deno** it is silently ignored, since Deno does not support Node's `--import` preload flag.

</note>

## Serving static files

The CLI can serve a directory of static files. No server entry is required — point `--static` at any folder:

```bash
npx srvx --static ./dist
```

If `--static` is omitted, srvx serves files from a `public/` directory when one exists, and skips static serving when it does not. Passing `--static` explicitly asserts the directory exists: srvx errors out if it is missing, rather than starting up and serving nothing.

When both a server entry and a static directory are present, static files take priority and unmatched requests fall through to your handler.

Static serving includes automatic `index.html` resolution, `.html` extension fallback (e.g. `/about` → `about.html`), common MIME types, gzip/Brotli compression, and path-traversal protection.

## Programmatic API

Both CLI modes are built on `srvx/loader`. The same loader is available to you, so you can build a dev server, a test harness, or a framework CLI that accepts any server entry srvx accepts — without reimplementing entry discovery or handler detection.

```ts
import { loadServerEntry } from "srvx/loader";
import { serve } from "srvx";

const loaded = await loadServerEntry({ entry: "./server.ts" });

if (loaded.notFound) {
  throw new Error("No server entry found");
}

if (!loaded.fetch) {
  throw new Error("Server entry exports no fetch handler");
}

serve({ fetch: loaded.fetch });
```

`loadServerEntry(opts)` imports a server entry module and resolves a web `fetch` handler from it. It never throws for a missing entry — check `notFound` on the result instead.

### Entry resolution

When `entry` is set, it is resolved against `dir` (or the current working directory). A `file://` URL is used as-is.

When `entry` is omitted, the loader searches `dir` for the first match of each of these base names, trying `.mjs`, `.js`, `.mts`, then `.ts` for each:

<steps level="4">

#### `server`

#### `server/index`

#### `src/server`

#### `server/server`

</steps>

Both lists are exported as `defaultEntries` and `defaultExts` if you need to reuse them (for example, to build a file watcher). If nothing matches, the result is `{ notFound: true }` with no `fetch` and no `module`.

### Handler resolution

Once the module is imported, the loader looks for a handler in this order:

<steps level="4">

#### `module.fetch`

#### `module.default.fetch`

#### `module.default.default.fetch` (a double-default from a transpiled CommonJS entry)

#### The `fetch` of a server the entry created by calling `serve()` (see [Loading entries that start a server](#loading-entries-that-start-a-server))

#### `module.default`, if it is a function taking fewer than two arguments

</steps>

If none match and `nodeCompat` is not disabled, a legacy Node.js `(req, res)` handler is wrapped into a fetch handler and `nodeCompat: true` is set on the result. Both `module.default` and a handler captured from `http.createServer()` are eligible.

### Loading entries that start a server

Most server entries do not export a handler — they start listening as a side effect of being imported. The loader intercepts the listen call, so importing the entry gives you its handler **without binding a port**.

If the entry calls srvx's own `serve()`, the loader hands you back that server instance as `srvxServer`, and its handler as `fetch`. The server is fully constructed but never listens. Because the entry's `serve()` call runs during the import, the instance does not exist yet when you pass options — use a getter to close the loop:

```ts
let server: Server | undefined;

const loaded = await loadServerEntry({
  entry: "./server.ts",
  get srvxServer() {
    return server;
  },
});

server = serve({ fetch: loaded.fetch!, port: 3000 });
```

Anything the entry exports alongside its handler is on `loaded.module`, so you can read its [server options](/guide/options) and merge them with your own.

If the entry instead calls `http.createServer(handler).listen()`, the loader captures `handler` and lets the entry run to completion without binding a port. Its listen callback still fires, so setup code that runs after listen is not skipped.

Pass `interceptHttpListen: false` to opt out. Note that this disables **both** interceptions — an entry that calls `serve()` will then really start listening on import.

<note>

Entries are intercepted one at a time — concurrent `loadServerEntry` calls are queued rather than run in parallel.

</note>

### Reloading an entry

The handler of an intercepted entry comes from a **side effect** of the import, and `import()` caches modules per URL. Loading the same entry twice in one process returns the cached module without re-running it, so the interception never fires and `fetch` comes back `undefined`. Entries that *export* a handler are unaffected — their handler lives on the cached module.

If you need to load an entry more than once (a watch mode, for example), give each load a unique entry URL:

```ts
import { pathToFileURL } from "node:url";

const url = pathToFileURL(resolve("./server.ts")).href;
const loaded = await loadServerEntry({ entry: `${url}?t=${Date.now()}` });
```

The query is ignored when checking that the file exists, and is preserved in the returned `url`.

<note>

Each reload leaves the previous module in the ESM cache for the lifetime of the process. For a long-running watch mode, restart a child process instead and let the runtime's own `--watch` handle reloads.

</note>

### `LoadOptions`

- **entry** — Path or `file://` URL of the server entry file. If omitted, the [default entries](#entry-resolution) are searched.
- **dir** — Base directory for resolving `entry` and for auto-discovery. Default is `"."`.
- **nodeCompat** — Set to `false` to disable upgrading a legacy Node.js `(req, res)` handler. The result then has no `fetch` when the entry only exports a Node.js handler. Default is `true`.
- **interceptHttpListen** — Set to `false` to import the entry without intercepting listen calls. Default is `true`.
- **srvxServer** — The srvx server instance to hand to the entry when its `serve()` call is intercepted. Define it as a getter when the instance is created after loading.
- **nodeServer** — The `node:http` server instance to return from an intercepted `listen()`. Defaults to `srvxServer`'s underlying Node.js server, or a stub that forwards to it once it exists.
- **onLoad** — Hook called with the imported module before the handler is resolved. Return a value to replace the module.

### `LoadedServerEntry`

- **fetch** — The resolved web fetch handler, or `undefined` if the entry exports none.
- **module** — The raw imported module. Use it to read options the entry exports next to its handler.
- **url** — The resolved `file://` URL of the loaded entry.
- **notFound** — `true` when no entry file could be located. `fetch` and `module` are then `undefined`.
- **nodeCompat** — `true` when the handler was upgraded from a legacy Node.js `(req, res)` handler. Serve it with `srvx/node`.
- **srvxServer** — The server instance the entry created via `serve()`, if that call was intercepted.

### TypeScript entries

The loader imports entries with a plain dynamic `import()`, so TypeScript support comes from the runtime:

- **Node.js** — requires v22.18+ or v24+ for `.ts` entries. The loader throws a targeted error on older versions rather than a raw `ERR_UNKNOWN_FILE_EXTENSION`.
- **Deno** and **Bun** — supported natively.
- **JSX** — needs a loader such as [jiti](https://github.com/unjs/jiti), preloaded with the [`--import`](#usage) flag.
