Skip to content

Repository files navigation

Jaunt

A high-performance, lightweight API Gateway core for Node.js, written in TypeScript.

Jaunt is built on three primitives with no heavy framework in the middle:

ConcernLibrary
HTTP serverNode.js native http module
Route matchingfind-my-way — Radix-tree router
Upstream proxyingfast-proxy — streaming HTTP proxy

Features

  • Zero-framework core — raw Node.js HTTP for maximum throughput
  • O(log n) route matching via a Radix-tree, powered by find-my-way
  • Streaming proxy — request and response bodies are never buffered, piped directly via fast-proxy
  • Onion-style middleware pipeline — Koa-inspired async plugin model with full pre/post proxy control
  • Dynamic route registration — add routes at any point, including after the server has started
  • Global and per-route plugins — cross-cutting concerns (logging, auth, rate limiting) at either scope
  • Fully typed — strict TypeScript throughout, with exported interfaces for all public contracts
  • Fluent API — chainable addRoute() calls for clean setup code

Requirements

  • Node.js >= 20.0.0
  • TypeScript >= 5.x

Installation

npm install jaunt

Quick Start

import{Gateway}from'jaunt';importtype{Plugin}from'jaunt';// A simple request logger pluginconstlogger: Plugin=async(ctx,next)=>{conststart=Date.now();awaitnext();console.log(`${ctx.req.method}${ctx.req.url}${ctx.res.statusCode} (${Date.now()-start}ms)`);};constgateway=newGateway({port: 3000,globalPlugins: [logger],});gateway.addRoute({method: 'GET',path: '/api/users/:id',upstream: 'http://user-service:3000',}).addRoute({method: 'POST',path: '/api/orders',upstream: 'http://order-service:4000',});awaitgateway.start();// 🚀 Jaunt Gateway listening on http://0.0.0.0:3000

// HTTP/1.1 upstream — unchanged, no migration needed

gateway.addRoute({method: 'GET',path: '/api/users/:id',upstream: 'http://user-service:3000',});

// HTTP/2 upstream — simple

gateway.addRoute({method: 'POST',path: '/api/orders',upstream: 'https://order-service:4000',http2: true,});

// HTTP/2 upstream — with custom timeouts and self-signed cert

gateway.addRoute({method: 'GET',path: '/api/inventory',upstream: 'https://inventory-service:5000',http2: {sessionTimeout: 30_000,requestTimeout: 5_000,rejectUnauthorized: false,// dev only},});

Request Lifecycle

Every request that hits the gateway goes through four steps in order:

Incoming Request
│
▼
1. Route Match (find-my-way Radix tree)
│ no match → 404
▼
2. Build GatewayContext
│ (params, query, upstream, state)
▼
3. Plugin Pipeline ──────────────────────────────────────────┐
│ Global plugins → Route plugins (onion model) │
│ Plugin short-circuits (no next()) → skip proxy │
▼ │
4. Upstream Proxy (fast-proxy streaming) │
│ upstream error → 502 │
▼ (post-proxy
Response streamed back to client code runs here)

API Reference

new Gateway(options?)

Creates a new gateway instance.

OptionTypeDefaultDescription
portnumber3000Port to listen on
hoststring'0.0.0.0'Hostname or IP to bind
globalPluginsPlugin[][]Plugins that run on every request, before route-level plugins
proxyTimeoutnumber30000Upstream request timeout in milliseconds

gateway.addRoute(route)

Registers a route. Returns this for chaining.

gateway.addRoute({method: 'GET',// HttpMethod — GET | POST | PUT | PATCH | DELETE | HEAD | OPTIONSpath: '/users/:id',// find-my-way path patternupstream: 'http://...',// Base URL of the upstream serviceplugins: [],// Optional route-level plugin array});

Routes can be added dynamically at any time, including after gateway.start() has been called.


gateway.start()

Starts the HTTP server and initialises the proxy connection pool.

awaitgateway.start();

Returns a Promise<void> that resolves once the server is listening.


gateway.stop()

Gracefully shuts down the server and drains the proxy connection pool.

awaitgateway.stop();

GatewayContext

The context object passed to every plugin.

interfaceGatewayContext{req: IncomingMessage;// Raw Node.js requestres: ServerResponse;// Raw Node.js responseparams: Record<string,string>;// Path parameters, e.g. { id: '42' }query: Record<string,string>;// Query string parametersupstream: string;// Upstream base URL for this routestate: Record<string,unknown>;// Free-form state bag for inter-plugin data}

Plugin

The middleware function signature.

typePlugin=(ctx: GatewayContext,next: ()=>Promise<void>)=>Promise<void>;
  • Call await next() to pass control to the next plugin (and eventually the proxy).
  • Omit next() to short-circuit the pipeline — the request will not be proxied.
  • Code beforenext() runs on the way in (pre-proxy).
  • Code afternext() runs on the way out (post-proxy).

Plugin Examples

Request Logger

constlogger: Plugin=async(ctx,next)=>{conststart=Date.now();console.log(`--> ${ctx.req.method}${ctx.req.url}`);awaitnext();console.log(`<-- ${ctx.res.statusCode} (${Date.now()-start}ms)`);};

API Key Authentication

constapiKeyAuth: Plugin=async(ctx,next)=>{if(ctx.req.headers['x-api-key']!==process.env.API_KEY){ctx.res.writeHead(401,{'Content-Type': 'application/json'});ctx.res.end(JSON.stringify({error: 'Unauthorized'}));return;// Do not call next() — pipeline stops here}awaitnext();};

Request URL Rewriting

// Map /api/users/:id → /users/:id on the upstreamconstrewritePath: Plugin=async(ctx,next)=>{ctx.req.url=`/users/${ctx.params['id']}`;awaitnext();};

Sharing State Between Plugins

constinjectRequestId: Plugin=async(ctx,next)=>{ctx.state['requestId']=crypto.randomUUID();awaitnext();};constuseRequestId: Plugin=async(ctx,next)=>{constid=ctx.state['requestId']asstring;ctx.req.headers['x-request-id']=id;awaitnext();};

Project Structure

jaunt/
├── src/
│ ├── index.ts # Public package entry point — re-exports all public API
│ ├── Gateway.ts # HTTP server, request lifecycle orchestration
│ ├── Router.ts # find-my-way wrapper — route registration and matching
│ ├── pipeline.ts # Onion-style async middleware composer
│ └── types.ts # All TypeScript interfaces and type definitions
├── examples/
│ └── basic-gateway.ts # Working demo with logger, auth, and path rewriting
├── package.json
└── tsconfig.json

Running the Example

The example proxies to JSONPlaceholder, a free public REST API.

npm install
npx ts-node examples/basic-gateway.ts

Then in another terminal:

# Public route — no auth
curl http://localhost:4000/posts/1
# Route with path param rewriting (/api/users/:id → /users/:id upstream)
curl http://localhost:4000/api/users/3
# Protected route — missing key, returns 401
curl http://localhost:4000/protected/data
# Protected route — valid key, proxied to upstream
curl -H "x-api-key: secret-key-123" http://localhost:4000/protected/data

Building

npm run build # Compile TypeScript to dist/
npm run build:watch # Watch mode
npm run clean # Remove dist/

License

MIT

About

A lightweight API Gateway core built on Node.js native HTTP, find-my-way, and fast-proxy.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages