srvx Documentation

repository·main·Indexed 21 days ago

https://github.com/h3js/srvx

A universal, zero-dependency server framework built on web standards. srvx provides a unified API for a seamless developer experience across Node.js, Deno, and Bun. It allows developers to define servers using a standard Fetch API pattern, offering extended request properties via ServerRequest and runtime-specific access for Bun, Deno, and Node.js. The framework supports background tasks via waitUntil, server lifecycle management, and deployment to AWS Lambda.

Tokens
27.9K
Snippets
82
Records
111
Agent score
73%

What's inside srvx

  1. How srvx handles runtime type declarations

    main

    To avoid dependency conflicts and tsc failures caused by overlapping global declarations (e.g., @types/bun and @types/deno both declaring globals), srvx uses minimal, custom type declarations for each runtime.

    These types are structurally compatible with official runtime objects (like Bun.Server, Deno.HttpServer, or AWS Lambda events), meaning you can pass real runtime objects into srvx functions without error. However, because these types only cover the properties that srvx specifically reads or forwards, they are not exhaustive.

    When to use official types: If you need access to the full API surface of a specific runtime object beyond what srvx uses, you should cast the object to its official type from the corresponding provider package.

  2. Configure trustProxy for reverse proxies

    main

    When running behind a reverse proxy (like Nginx or an AWS ALB), set trustProxy to ensure request.url and request.ip are derived correctly from X-Forwarded-* headers.

    • false (default): Ignores headers; uses real connection info.
    • true: Trusts all hops in the chain.
    • "loopback": Trusts only hops on a loopback address (127.0.0.0/8 or ::1).
    • string[]: An allowlist of trusted addresses.

    srvx uses hop-aware, right-to-left resolution. It treats addresses in the trusted set as proxies you control and identifies the client as the rightmost address not in the trusted set.

    import { serve } from "srvx";
    
    serve({
      // Behind a reverse proxy you control:
      trustProxy: true,
      fetch: (request) => new Response(new URL(request.url).protocol),
    });
  3. How srvx handles Node.js compatibility

    main

    srvx provides Node.js support by using a lightweight proxy system that bridges the gap between Node.js's IncomingMessage/ServerResponse and the Web standard Request/Response interfaces.

    Key Mechanisms

    • NodeRequest: Wraps node:http.IncomingMessage to expose a standard Request interface. It uses a proxy (NodeReqHeadersProxy) for headers and lazily starts reading the body as a ReadableStream upon the first access to request.body.
    • sendNodeResponse: Handles the Response object returned by the server's fetch method. It sets status, status text, and headers (properly splitting set-cookie headers), and streams the response body to the Node.js response.
    • Zero Patching: srvx avoids patching or modifying the global Request and Response constructors by default, keeping runtime natives untouched and ensuring there is only one source of truth (the Node.js request instance).
  4. Understand per-runtime option behavior in srvx

    main

    While srvx aims for consistent behavior across runtimes, certain options behave differently depending on whether you are using Node.js, Bun, Deno, or edge runtimes like Cloudflare or Bunny.

    Key Runtime Differences

    • close(true) (Force Close): Works as expected on Node and Bun. On Deno, the force argument is currently ignored, resulting in a graceful close instead.
    • trustProxy: This option is only applied when using the Node, AWS Lambda, Bun, and Deno adapters. It is ignored on Cloudflare, Bunny, and generic/service-worker adapters.
    • maxRequestBodySize:
      • Node/Deno: Enforced by srvx (unlimited by default).
      • Bun: Forwarded to Bun's native option (which defaults to 128 MiB even if unset in srvx).
      • Note: This option is dropped (not applied) when running via the CLI loader on any runtime.
    • manual: This option has no effect on module-worker runtimes (such as Cloudflare module syntax or Bunny) because there is no listening step to defer.
    • gracefulShutdown: Supported on Node, Deno, and Bun. It is a no-op on Cloudflare, Bunny, and service-worker runtimes.
    • Cloudflare env bindings: Bindings are only available via request.runtime.cloudflare.env when using module-worker syntax. They are unavailable when using service-worker syntax (the global fetch listener).
    • WebSockets: srvx does not handle HTTP upgrade requests by default. For WebSocket support, use crossws.
  5. Use the ServerRequest type for extended request properties

    main

    While srvx uses the standard Request object, you can use the ServerRequest type exported from srvx to access extended properties provided by the runtime. These properties include client IP, TLS state, runtime-specific server instances, and lifecycle management methods.

    import { serve, type ServerRequest } from "srvx";
    
    serve({
      fetch: (request: ServerRequest) => {
        // Access extended properties here
      }
    });
  6. Extend the server with middleware and plugins

    main

    You can extend server behavior using middleware or plugins:

    • Middleware: An array of functions (request, next) => Response executed before the main handler.
    • Plugins: Synchronous functions that receive the server instance. They are useful for registering middleware or augmenting the server instance during initialization. All plugin-registered middleware is applied before the first request is handled.
    import { serve } from "srvx";
    
    // Using middleware
    serve({
      middleware: [
        (request, next) => {
          console.log(`[${request.method}] ${request.url}`);
          return next();
        },
      ],
      fetch: () => new Response("👋 Hello there!"),
    });
    
    // Using plugins
    const myPlugin = (server) => {
      server.options.middleware.push((request, next) => next());
    };
    
    serve({
      plugins: [myPlugin],
      fetch: () => new Response("👋 Hello there!"),
    });
  7. How middleware and plugins work in srvx

    main

    srvx uses Middleware and Plugins to extend server functionality.

    Middleware

    Middleware are functions that wrap the request/response cycle. They run in the order they appear in the middleware array. Each middleware receives a req and a next function. Calling next() passes control to the next middleware or the fetch handler. If a middleware returns a response without calling next(), it short-circuits the chain, and subsequent middleware/handlers will not run.

    Plugins

    Plugins are functions that receive the server instance and can modify its configuration (like adding middleware) before the server starts. They run in the order they appear in the plugins array. If a plugin pushes middleware to server.options.middleware, that middleware is appended to the end of the existing array.

    Execution Order

    1. Plugins run first (in order).
    2. Middleware run second (in order, wrapping the fetch handler).
    3. fetch handler runs last (at the center of the chain).
    import { serve, type ServerMiddleware, type ServerPlugin } from "srvx";
    
    const xPoweredBy: ServerMiddleware = async (req, next) => {
      const res = await next();
      res.headers.set("X-Powered-By", "srvx");
      return res;
    };
    
    const devLogs: ServerPlugin = (server) => {
      if (process.env.NODE_ENV === "production") {
        return;
      }
      console.log(`Logger plugin enabled!`);
      server.options.middleware.push((req, next) => {
        console.log(`[request] [${req.method}] ${req.url}`);
        return next();
      });
    };
    
    serve({
      middleware: [xPoweredBy],
      plugins: [devLogs],
      fetch(request) {
        return new Response(`👋 Hello there.`);
      },
    });
  8. Run srvx using different runtimes

    main

    After creating your server handler, you can start the server using your preferred runtime's CLI tool.

    Node.js

    Use npx, pnpx, or yarn dlx to run the srvx package.

    Deno

    Use deno -A npm:srvx to run with all permissions.

    Bun

    Use bunx --bun srvx for native Bun execution.

    # Node.js
    $ npx srvx       # npm
    $ pnpx srvx      # pnpm
    $ yarn dlx srvx  # yarn
    
    # Deno
    $ deno -A npm:srvx
    
    # Bun
    $ bunx --bun srvx
  9. Enable HTTPS in srvx

    main

    To serve over HTTPS, provide a tls object containing cert and key in the serve configuration. srvx will automatically switch the protocol to https.

    Values for cert and key can be either file paths or inline PEM content. If the string starts with -----BEGIN , it is treated as inline PEM; otherwise, it is treated as a file path.

    Security Note: Never commit private keys to version control. Use environment variables or a secret manager to load them.

    import { serve } from "srvx";
    
    serve({
      tls: { cert: "./server.crt", key: "./server.key" },
      fetch: () => new Response("👋 Secure hello!"),
    });
  10. Implement a Fetch Handler with srvx

    main

    A request handler in srvx is defined using the fetch key in the configuration object passed to serve. The handler follows the standard Fetch API pattern: it accepts a Request object as input and must return a Response object or a Promise that resolves to a Response if the handler is async.

    import { serve } from "srvx";
    
    serve({
      async fetch(request) {
        return new Response(
          `
            <h1>👋 Hello there</h1>
            <p>You are visiting ${request.url} from ${request.ip}</p>
          `,
          { headers: { "Content-Type": "text/html" } },
        );
      },
    });
  11. Handle body size limit errors

    main

    When a body limit is exceeded, the helpers throw an error of type BodyTooLargeError created by createBodyTooLargeError. This error has a stable shape that allows you to map it to an HTTP 413 Payload Too Large response without string matching.

    Default Error Shape:

    PropertyValue
    code"ERR_BODY_TOO_LARGE"
    statusCode413
    status413

    To use a custom error instead of the default, pass a createError function in the options object to limitRequestBody or limitBodyStream.

    import { createBodyTooLargeError, limitRequestBody } from "srvx/body-limit";
    
    const safeRequest = limitRequestBody(request, 1024 * 1024);
    try {
      return Response.json(await safeRequest.json());
    } catch (error) {
      if (error.code === "ERR_BODY_TOO_LARGE") {
        return new Response("Payload Too Large", { status: error.statusCode });
      }
      throw error;
    }
    
    // Or construct the error directly to enforce a limit elsewhere:
    throw createBodyTooLargeError(1024 * 1024);
  12. Use srvx fetch mode to make HTTP requests

    main

    The fetch mode (or curl alias) allows you to test your server by making requests directly from the CLI. It behaves similarly to curl.

    Common Fetch Commands

    • Fetch default entry: srvx fetch
    • Fetch specific path: srvx fetch /api/users
    • Fetch with specific entry: srvx fetch --entry ./server.ts /api/users
    • POST request: srvx fetch -X POST /api/users
    • Add headers: srvx fetch -H "Content-Type: application/json" /api
    • Send body: srvx fetch -d '{"name":"foo"}' /api
    • Body from stdin: echo '{"name":"foo"}' | srvx fetch -d @- /api
    • Verbose output: srvx fetch -v /api/users (shows headers)

    Exit Codes

    • 0: Success (2xx response)
    • 22: Failure (non-2xx response)
    # POST a JSON body to an API
    $ srvx fetch -X POST -H "Content-Type: application/json" -d '{"name":"foo"}' /api