# Server Options

> Provide additional options to customize listening server.

When starting a new server, in addition to main `fetch` handler, you can provide additional options to customize listening server.

```js
import { serve } from "srvx";

serve({
  // Generic options
  port: 3000,
  hostname: "localhost",

  // Runtime specific options
  node: {},
  bun: {},
  deno: {},

  // Main server handler
  fetch: () => new Response("👋 Hello there!"),
});
```

There are two kind of options:

- Generic options: Top level options are intended to have exactly same functionality regardless of runtime
- Runtime specific: Allow customizing more runtime specific options

## Generic Options

### `port`

The port server should be listening to.

Default is value of `PORT` environment variable or `3000`.

<tip>

You can set the port to `0` to use a random port.

</tip>

### `hostname`

The hostname (IP or resolvable host) server listener should bound to.

Default is value of the `HOST` environment variable, if set. When neither `hostname` nor `HOST` is provided, the server will **listen to all network interfaces** by default.

<important>

If you are running a server that should not be exposed to the network, use `localhost`.

</important>

### `reusePort`

Enabling this option allows multiple processes to bind to the same port, which is useful for load balancing.

<note>

Despite Node.js built-in behavior that has `exclusive` flag enabled by default, srvx uses non-exclusive mode for consistency.

</note>

### `silent`

If enabled, no server listening message will be printed (enabled by default when `TEST` environment variable is set).

### `manual`

If set to `true`, the server will **not** start listening automatically. You must call [`server.serve()`](/guide/server#serverserve) yourself to begin accepting connections. Useful when you need to fully construct the server before it starts.

```js
import { serve } from "srvx";

const server = serve({
  manual: true,
  fetch: () => new Response("👋 Hello there!"),
});

// ...later
await server.serve();
await server.ready();
```

<note>

`manual` is meaningless on module-worker style runtimes (Cloudflare module syntax, Bunny), where the runtime drives the request lifecycle and there is no listening step to defer.

</note>

### `middleware`

An array of [middleware](/guide/middleware) handlers to run before the main `fetch` handler.

```js
import { serve } from "srvx";

serve({
  middleware: [
    (request, next) => {
      console.log(`[${request.method}] ${request.url}`);
      return next();
    },
  ],
  fetch: () => new Response("👋 Hello there!"),
});
```

<read-more to="/guide/middleware" title="Middleware">



</read-more>

### `plugins`

An array of [plugins](/guide/middleware) to extend the server. A plugin is a **synchronous** function that receives the server instance and can register middleware, hook into the lifecycle, or augment requests.

```js
import { serve } from "srvx";

const myPlugin = (server) => {
  server.options.middleware.push((request, next) => next());
};

serve({
  plugins: [myPlugin],
  fetch: () => new Response("👋 Hello there!"),
});
```

<note>

Plugins are sync-only (they return `void`). All plugin-registered middleware is therefore in place before the first request is handled.

</note>

### `protocol`

The protocol to use for the server.

Possible values are `http` or `https`.

If `protocol` is not set, Server will use `http` as the default protocol or `https` if both `tls.cert` and `tls.key` options are provided.

### `tls`

TLS server options.

**Example:**

```js
import { serve } from "srvx";

serve({
  tls: { cert: "./server.crt", key: "./server.key" },
  fetch: () => new Response("👋 Hello there!"),
});
```

**Options:**

- `cert`: Path or inline content for the certificate in PEM format (required).
- `key`: Path or inline content for the private key in PEM format (required).
- `passphrase`: Passphrase for the private key (optional).

<tip>

You can pass inline `cert` and `key` values in PEM format starting with `-----BEGIN `.

</tip>

Client certificates (mutual TLS) are available through the [`mtls()` plugin](/guide/tls#mutual-tls-mtls).

<read-more to="/guide/tls" title="TLS & mutual TLS">



</read-more>

### `onError`

Runtime agnostic error handler.

<note>

This handler will take over the built-in error handlers of Deno and Bun.

</note>

**Example:**

```js
import { serve } from "srvx";

serve({
  fetch: () => new Response("👋 Hello there!"),
  onError(error) {
    return new Response(`<pre>${error}\n${error.stack}</pre>`, {
      headers: { "Content-Type": "text/html" },
    });
  },
});
```

### `maxRequestBodySize`

Maximum allowed size **in bytes** for the request body. Defaults to `undefined` (no limit).

As the body is read, its accumulated length is tracked and, once it exceeds the limit, reading is aborted and rejects with a `413`-style error. The error carries `statusCode: 413`, `status: 413` and `code: "ERR_BODY_TOO_LARGE"`, so a handler (or [`onError`](#onerror)) can map it to an HTTP `413 Payload Too Large` response.

The limit covers both buffered reads (`request.text()` / `request.json()`) and the streamed body (`request.body`, and therefore `request.arrayBuffer()` / `.blob()` / `.bytes()` / `.formData()`).

**Example:**

```js
import { serve } from "srvx";

serve({
  maxRequestBodySize: 1024 * 1024, // 1 MiB
  fetch: async (request) => {
    try {
      return Response.json(await request.json());
    } catch (error) {
      if (error.code === "ERR_BODY_TOO_LARGE") {
        return new Response("Payload Too Large", { status: 413 });
      }
      throw error;
    }
  },
});
```

<note>

Runtime support:

- **Node**: enforced by srvx (the request body stream is size-limited).
- **Bun**: forwarded to Bun's native [`maxRequestBodySize`](https://bun.sh/docs/api/http), enforced by Bun (responds with `413` before the handler runs).
- **Deno**: enforced by srvx (`Deno.serve` has no native option).

</note>

### `trustProxy`

Whether to trust `X-Forwarded-*` headers (`X-Forwarded-Proto`, `X-Forwarded-Host`, `X-Forwarded-For`, and the HTTP/2 `:scheme`) when deriving `request.url` and `request.ip`. Defaults to `false`.

Any client can send `X-Forwarded-Proto: https`, `X-Forwarded-Host` or `X-Forwarded-For`, so trusting them lets a request masquerade as `https:`, forge its host, or spoof its client IP. Only enable this when a proxy you control sits in front and overwrites the headers.

- `false` (default): ignore the headers; use the real connection protocol, the on-the-wire `Host` header and the socket peer address.
- `true`: always trust the headers.
- `"loopback"`: trust them only when the proxy connects from a loopback address (`127.0.0.0/8` or `::1`).
- `string[]`: trust them only when the proxy's address is in the list.

**Example:**

```js
import { serve } from "srvx";

serve({
  // Behind a reverse proxy you control (e.g. Nginx, a load balancer):
  trustProxy: true,
  fetch: (request) => new Response(new URL(request.url).protocol),
});
```

<note>

Applies to the Node, AWS Lambda, Bun and Deno adapters.

</note>

## Runtime Specific Options

### Node.js

**Example:**

```js
import { serve } from "srvx";

serve({
  node: {
    maxHeaderSize: 16384 * 2, // Double default
    ipv6Only: true, // Disable dual-stack support
    // http2: false // Disable http2 support (enabled by default in TLS mode)
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

<read-more>

See Node.js documentation for [ServerOptions](https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener) and [ListenOptions](https://nodejs.org/api/net.html#serverlistenoptions-callback) for all available options.

</read-more>

### Bun

**Example:**

```js
import { serve } from "srvx";

serve({
  bun: {
    error(error) {
      return new Response(`<pre>${error}\n${error.stack}</pre>`, {
        headers: { "Content-Type": "text/html" },
      });
    },
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

<read-more to="https://bun.sh/docs/api/http">

See Bun HTTP documentation for all available options.

</read-more>

### Deno

**Example:**

```js
import { serve } from "srvx";

serve({
  deno: {
    onError(error) {
      return new Response(`<pre>${error}\n${error.stack}</pre>`, {
        headers: { "Content-Type": "text/html" },
      });
    },
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

<read-more to="https://docs.deno.com/api/deno/~/Deno.ServeOptions">

See Deno serve documentation for all available options.

</read-more>

### Service Worker

Options for the service worker adapter (`srvx/service-worker`).

```js
import { serve } from "srvx/service-worker";

serve({
  serviceWorker: {
    url: "/sw.js", // path to the service worker file to register
    scope: "/", // service worker scope
  },
  fetch: () => new Response("👋 Hello there!"),
});
```

- `url`: The path to the service worker file to be registered.
- `scope`: The scope of the service worker.

## Per-runtime Support

srvx aims for identical behavior across runtimes, but a few options depend on capabilities the underlying runtime does not expose. The table below lists the intentional differences.

<table>
<thead>
  <tr>
    <th>
      Option / feature
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        close(true)
      </code>
      
       (force close)
    </td>
    
    <td>
      Honored on Node and Bun. On <strong>
        Deno
      </strong>
      
       the force argument is currently ignored — <code>
        close(true)
      </code>
      
       behaves like a graceful <code>
        close()
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        trustProxy
      </code>
    </td>
    
    <td>
      Applies to the <strong>
        Node, AWS Lambda, Bun and Deno
      </strong>
      
       adapters only. Ignored on Cloudflare, Bunny and the generic/service-worker adapters.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        maxRequestBodySize
      </code>
    </td>
    
    <td>
      Node/Deno enforced by srvx (unlimited by default). <strong>
        Bun
      </strong>
      
       forwards it to Bun's native option, which has its own <strong>
        128 MiB
      </strong>
      
       default even when unset. Dropped (not applied) when running under the CLI loader on any runtime.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        manual
      </code>
    </td>
    
    <td>
      Meaningless on module-worker runtimes (Cloudflare module syntax, Bunny) — there is no listening step to defer.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        gracefulShutdown
      </code>
    </td>
    
    <td>
      Supported on Node, Deno and Bun. No-op on Cloudflare, Bunny and service-worker runtimes.
    </td>
  </tr>
  
  <tr>
    <td>
      Cloudflare <code>
        env
      </code>
      
       bindings
    </td>
    
    <td>
      <code>
        request.runtime.cloudflare.env
      </code>
      
       is only populated in <strong>
        module-worker syntax
      </strong>
      
      . In service-worker syntax (the global <code>
        fetch
      </code>
      
       listener) bindings are unavailable.
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        <code>
          upgrade
        </code>
        
         / WebSocket
      </strong>
    </td>
    
    <td>
      srvx does <strong>
        not
      </strong>
      
       handle HTTP <code>
        upgrade
      </code>
      
       requests on <strong>
        any
      </strong>
      
       runtime — WebSocket upgrades never reach your <code>
        fetch
      </code>
      
       handler. Use <a href="https://crossws.h3.dev/" rel="nofollow">
        crossws
      </a>
      
       for WebSocket support.
    </td>
  </tr>
</tbody>
</table>

### Deno version

srvx targets **Deno 2.x** (CI runs against Deno 2.7.x).

<important>

Under the Bunny runtime, a root `srvx` import resolves the `deno` export condition (Bunny is Deno-based) and would give you the Deno adapter. Always import the Bunny adapter explicitly via `srvx/bunny`.

</important>

## Public Subpaths

In addition to the main `srvx` entry and the runtime adapters, srvx exposes several public subpaths:

<table>
<thead>
  <tr>
    <th>
      Subpath
    </th>
    
    <th>
      Exports
    </th>
    
    <th>
      Use when
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        srvx/static
      </code>
    </td>
    
    <td>
      <code>
        serveStatic
      </code>
      
       middleware
    </td>
    
    <td>
      Serving static files. <strong>
        Node-API-only
      </strong>
      
       (uses <code>
        node:fs
      </code>
      
       / <code>
        node:zlib
      </code>
      
      ) despite the neutral name — works only on runtimes with Node compatibility.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        srvx/log
      </code>
    </td>
    
    <td>
      <code>
        log()
      </code>
      
       middleware
    </td>
    
    <td>
      Adding request logging (used by the CLI).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        srvx/loader
      </code>
    </td>
    
    <td>
      <code>
        loadServerEntry
      </code>
      
      , related types
    </td>
    
    <td>
      Resolving and loading a server entry file (as the CLI does).
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        srvx/cli
      </code>
    </td>
    
    <td>
      <code>
        main
      </code>
      
      , <code>
        cliFetch
      </code>
      
      , CLI types
    </td>
    
    <td>
      Building a custom CLI on top of srvx.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        srvx/generic
      </code>
    </td>
    
    <td>
      <code>
        serve
      </code>
      
      , <code>
        FastURL
      </code>
      
      , <code>
        FastResponse
      </code>
    </td>
    
    <td>
      A web-standard (<code>
        fetch
      </code>
      
      /<code>
        Response
      </code>
      
      ) runtime with no dedicated adapter.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        srvx/service-worker
      </code>
    </td>
    
    <td>
      <code>
        serve
      </code>
      
      , <code>
        FastURL
      </code>
      
      , <code>
        FastResponse
      </code>
    </td>
    
    <td>
      Running inside a browser/service-worker environment.
    </td>
  </tr>
</tbody>
</table>

## TypeScript Types

Runtime-specific option and context types are typed against each runtime's own type packages, which srvx does **not** bundle. To get full typing for `options.bun`, `options.deno`, `request.runtime.cloudflare`, `request.runtime.awsLambda` (and similar), install the relevant `devDependencies` in your project:

- **Bun** (`options.bun`): [`@types/bun`](https://www.npmjs.com/package/@types/bun) (or `bun-types`)
- **Deno** (`options.deno`, `request.runtime.deno`): [`@types/deno`](https://www.npmjs.com/package/@types/deno)
- **Cloudflare** (`request.runtime.cloudflare`): [`@cloudflare/workers-types`](https://www.npmjs.com/package/@cloudflare/workers-types)
- **AWS Lambda** (`request.runtime.awsLambda`): [`@types/aws-lambda`](https://www.npmjs.com/package/@types/aws-lambda)

Without these, the corresponding fields fall back to `any` (with `skipLibCheck`) or raise type errors (without it).
