Skip to main content

Core Types

RouteInfo

interface RouteInfo {
  hostname: string;
  port: number;
}
Represents a mapping from a hostname to a local port. Used by the proxy server to route incoming requests.
hostname
string
The hostname to match against the Host header (e.g., “api.localhost”, “app.myproject.localhost”)
port
number
The local port number where the application is listening (e.g., 3000, 4000)
Example:
const route: RouteInfo = {
  hostname: "api.localhost",
  port: 3000,
};

RouteMapping

interface RouteMapping extends RouteInfo {
  pid: number;
}
Extends RouteInfo with process tracking. Used internally by RouteStore to manage route ownership and lifecycle.
pid
number
Process ID of the application that registered this route. Use process.pid for the current process, or 0 for system-managed routes that should never be automatically cleaned up.
Example:
const mapping: RouteMapping = {
  hostname: "api.localhost",
  port: 3000,
  pid: process.pid,
};

ProxyServerOptions

interface ProxyServerOptions {
  getRoutes: () => RouteInfo[];
  proxyPort: number;
  onError?: (message: string) => void;
  tls?: {
    cert: Buffer;
    key: Buffer;
    SNICallback?: (
      servername: string,
      cb: (err: Error | null, ctx?: import("node:tls").SecureContext) => void
    ) => void;
  };
}
Configuration options for createProxyServer().
getRoutes
() => RouteInfo[]
required
Callback function invoked on every request to retrieve the current route table. This enables dynamic routing where routes can be added/removed without restarting the server.
proxyPort
number
required
The port number the proxy server is listening on. Used to construct correct URLs in error pages and route listings.
onError
(message: string) => void
Optional error logger called when proxy errors occur. Defaults to console.error if not provided.
tls
TLSOptions
Optional TLS configuration. When provided, enables HTTP/2 over TLS with HTTP/1.1 fallback.
Example:
import * as fs from "node:fs";

const options: ProxyServerOptions = {
  getRoutes: () => [
    { hostname: "api.localhost", port: 3000 },
    { hostname: "app.localhost", port: 4000 },
  ],
  proxyPort: 8080,
  onError: (msg) => console.error(`[proxy] ${msg}`),
  tls: {
    cert: fs.readFileSync("cert.pem"),
    key: fs.readFileSync("key.pem"),
  },
};

ProxyServer

type ProxyServer = http.Server | net.Server;
Return type of createProxyServer(). When TLS is disabled, returns an http.Server. When TLS is enabled, returns a net.Server that wraps both HTTP/2 and HTTP/1.1 servers. Example:
import type { ProxyServer } from "portless";

let server: ProxyServer;

if (useTLS) {
  server = createProxyServer({ ...options, tls: tlsConfig });
} else {
  server = createProxyServer(options);
}

server.listen(8080);

Error Types

RouteConflictError

class RouteConflictError extends Error {
  readonly hostname: string;
  readonly existingPid: number;
  
  constructor(hostname: string, existingPid: number);
}
Thrown by RouteStore.addRoute() when attempting to register a hostname that’s already in use by a live process (unless force: true is specified).
hostname
string
The hostname that caused the conflict
existingPid
number
Process ID of the process that currently owns the hostname
Example:
import { RouteStore, RouteConflictError } from "portless";

const store = new RouteStore("/tmp/portless");

try {
  store.addRoute("api.localhost", 3000, process.pid);
} catch (err) {
  if (err instanceof RouteConflictError) {
    console.error(
      `Cannot register ${err.hostname}: already owned by PID ${err.existingPid}`
    );
    
    // Option 1: Wait for the other process to exit
    // Option 2: Use force flag to override
    store.addRoute(err.hostname, 3000, process.pid, true);
  } else {
    throw err;
  }
}

Constants

PORTLESS_HEADER

const PORTLESS_HEADER = "X-Portless";
HTTP response header added to all proxied responses. Used to identify that a response came through a Portless proxy (useful for health checks and debugging). Example:
fetch("http://api.localhost:8080/health")
  .then(res => {
    if (res.headers.get("X-Portless")) {
      console.log("Request went through Portless proxy");
    }
  });

File Permissions

export const FILE_MODE = 0o644;        // Route/state files (-rw-r--r--)
export const DIR_MODE = 0o755;         // User state directory (drwxr-xr-x)
export const SYSTEM_DIR_MODE = 0o1777; // System state directory (drwxrwxrwt)
export const SYSTEM_FILE_MODE = 0o666; // System state files (-rw-rw-rw-)
File and directory permission modes used by RouteStore.

Import Examples

import {
  createProxyServer,
  RouteStore,
  RouteConflictError,
  PORTLESS_HEADER,
} from "portless";

import type {
  ProxyServer,
  ProxyServerOptions,
  RouteInfo,
  RouteMapping,
} from "portless";

CommonJS

const {
  createProxyServer,
  RouteStore,
  RouteConflictError,
  PORTLESS_HEADER,
} = require("portless");

Type Guards

isValidRoute (Internal)

While not exported, here’s how RouteStore validates route data:
function isValidRoute(value: unknown): value is RouteMapping {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as RouteMapping).hostname === "string" &&
    typeof (value as RouteMapping).port === "number" &&
    typeof (value as RouteMapping).pid === "number"
  );
}
You can implement similar validation in your code:
function validateRouteInfo(data: unknown): RouteInfo {
  if (
    typeof data !== "object" ||
    data === null ||
    typeof (data as RouteInfo).hostname !== "string" ||
    typeof (data as RouteInfo).port !== "number"
  ) {
    throw new TypeError("Invalid RouteInfo object");
  }
  return data as RouteInfo;
}

API Overview

Getting started with the programmatic API

createProxyServer

Create proxy servers

RouteStore

Manage route mappings

Build docs developers (and LLMs) love