Start as an API. Grow into a mesh.
OpenMesh is a TypeScript-first Node.js runtime for building ordinary HTTP APIs that can grow into an application-native service mesh: discovery, routing, backpressure, retries, circuits, streaming, traffic policy, live configuration, and observability stay inside the runtime instead of requiring a sidecar on every service.
npm install openmesh-nodev0.5.0 is the current npm release. It ships ESM + CommonJS + declarations and keeps the native HTTP core at zero runtime dependencies.
v0.5.0 is released. The current developer surface is intentionally small: typed app.get/post/... for normal APIs, then app.mesh('service') when the application grows. See Application-native Mesh and Advanced HTTP Contracts.
Website · Docs · Get started · 0.5 architecture · Application-native mesh · Databases · Control plane · Observability · API
Normal API development stays small:
import openmesh, { created } from 'openmesh-node';
import { bodyParser } from 'openmesh-node/plugins';
const app = openmesh();
app.use(bodyParser());
app.post('/users/:id', {
body: NewUser,
response: {
201: User,
409: Problem
}
}, async ({ body, params }) => {
return created(await users.create({
id: params.id,
...body
}));
});Bring the database library you already use. OpenMesh does not add a query language:
import { database } from 'openmesh-node/db';
const db = database(prisma, {
connect: client => client.$connect(),
disconnect: client => client.$disconnect(),
transaction: (client, work) => client.$transaction(work)
});
app.register(db);
app.get('/users', async () => db.client.user.findMany());The same resource API works with connectionless clients such as Drizzle, explicit lifecycle clients such as TypeORM, and transaction builders such as Kysely. The original client type is preserved. See Databases and ORMs.
When that application splits into services, the programming model grows instead of changing:
const app = openmesh({
service: 'gateway',
mesh: {
control: {
url: process.env.OPENMESH_CONTROL_URL,
token: process.env.OPENMESH_TOKEN
},
services: {
payments: {
traffic: {
split: [
{ match: { version: 'v1' }, weight: 90 },
{ match: { version: 'v2' }, weight: 10 }
]
}
}
}
}
});
const payments = app.mesh('payments');
const charge = await payments.post('/charges', {
key: user.id,
body: {
userId: user.id,
amount: order.total
}
});That call reuses OpenMesh's existing discovery watch, per-service bulkhead, deadlines, retry policy, circuit state, peer routing, tracing, and metrics. Traffic rules, weighted canaries, locality preference/failover, and live control-plane policy updates sit above the same warm service pool. There is no sidecar hop.
OpenMesh 0.5 also includes SPIFFE-style workload identity and service-to-service mTLS: the same application identity config protects inbound HTTPS and authenticates outbound app.mesh() calls. OpenMesh can hot-rotate already-issued workload certificates without restarting the process; certificate issuance and revocation distribution remain external, so it should not be described as a drop-in Istio/Linkerd replacement.
Most Node.js HTTP libraries stop at the server boundary. OpenMesh keeps going.
| Problem | OpenMesh primitive |
|---|---|
| How do I call healthy service instances? | Discovery-backed ServicePool |
| What happens when downstream is overloaded? | Bounded admission, queues, bulkheads, adaptive concurrency |
| How do I fail over without replaying unsafe work? | Deadlines, retry policy, circuit breaking, idempotency-aware rules |
| How do long-lived responses behave? | True streaming with explicit post-header no-replay semantics |
| How does topology update without restarts? | Registration leases + push watches |
| How do I change config safely at runtime? | Immutable snapshots + revision/epoch CAS |
| How do I use my existing ORM or SQL client? | Typed database resources with lifecycle, health and optional transaction adapters |
| How do I persist the control plane? | Durable Redis adapters |
| How do services authenticate each other? | SPIFFE-style workload identity + mutual TLS + inbound allow-lists |
| How do I limit control-plane access? | Scoped credentials + service/namespace boundaries |
| How do I export telemetry? | Request/server/peer lifecycle events + OpenTelemetry-compatible metrics bridge |
The goal is a small runtime with clear failure semantics—not a hidden cluster platform.
┌──────────────────────────┐
│ Control Plane │
│ registration • config │
│ scopes • watches • CAS │
└────────────┬─────────────┘
│
in-memory / Redis
│
▼
┌──────────────┐ ┌───────────────────────────────┐
│ Your service │ ───▶ │ OpenMesh runtime │
│ routes/hooks │ │ │
└──────────────┘ │ ServicePool │
│ └─ discovery watch │
│ └─ per-service bulkhead │
│ └─ adaptive concurrency │
│ │
│ PeerPool │
│ └─ P2C / rendezvous │
│ └─ queue / deadline │
│ └─ retry / circuit │
│ └─ buffered / streaming │
└───────────────┬───────────────┘
│ HTTP
┌─────────────┼─────────────┐
▼ ▼ ▼
service-a service-b service-c
No sidecar is required. No Go process sits in the request path. Redis, OpenTelemetry, Express, and Fastify integrations are optional boundaries, not core runtime dependencies.
Requires Node.js 22+.
import openmesh from 'openmesh-node';
import { jsonBody } from 'openmesh-node/plugins';
const app = openmesh();
app.use(jsonBody());
app.get('/', () => ({ hello: 'OpenMesh' }));
app.get('/users/:id', ctx => ({ id: ctx.params.id }));
app.post('/echo', ctx => ctx.requestBody);
await app.listen({ port: 3000 });
process.once('SIGTERM', () => app.close());
process.once('SIGINT', () => app.close());Run:
node server.mjsCommonJS is supported too:
const openmesh = require('openmesh-node');OpenMesh can own service registration, discovery, routing, and per-service pressure isolation.
Start a control plane:
import openmesh from 'openmesh-node';
import { controlPlane } from 'openmesh-node/services';
const control = openmesh();
control.register(controlPlane({
token: process.env.OPENMESH_TOKEN
}));
await control.listen({ port: 4000 });Register a service:
import {
ControlClient,
serviceRegistration
} from 'openmesh-node/services';
const client = new ControlClient({
url: 'http://127.0.0.1:4000/_mesh',
token: process.env.OPENMESH_TOKEN
});
app.onClose(() => client.close());
app.register(serviceRegistration({
client,
service: 'users',
id: 'users-a',
ttl: 30_000,
url: address => 'http://127.0.0.1:' + address.port
}));Then call the service through a managed pool:
const users = await client.service('users', {
maxInflight: 64,
maxQueue: 128,
adaptiveConcurrency: true
});
const response = await users.request('/users/42', {
key: '42'
});
console.log(response.peer.id, response.json());Membership changes are followed automatically. Each service gets isolated queue, concurrency, circuit, and adaptive state.
const stream = await users.requestStream('/events', {
timeout: 5_000,
idleTimeout: 30_000
});
stream.body.pipe(process.stdout);For streaming calls, the normal timeout covers admission + response headers. After non-5xx headers are handed to the caller, OpenMesh treats the response as committed and will not replay it on another peer if the body later fails.
That behavior matters for SSE, LLM responses, large downloads, and any operation where invisible retries would be dangerous.
The default registry/config stores are in-memory. For durable deployments, use the Redis adapters:
import { createClient } from 'redis';
import {
RedisRegistryAdapter,
RedisConfigAdapter
} from 'openmesh-node/services/redis';
const redis = createClient({
url: process.env.REDIS_URL
});
await redis.connect();
control.register(controlPlane({
credentials,
registry: new RedisRegistryAdapter({
client: redis,
prefix: 'openmesh-prod'
}),
config: new RedisConfigAdapter({
client: redis,
prefix: 'openmesh-prod'
})
}));Lease ownership, renewal, deregistration, configuration CAS, and revision updates use Redis-side atomic transitions. Registry watches combine Pub/Sub with TTL-expiry detection, so Redis keyspace notifications are not required.
Simple deployments can use one token. Larger deployments can scope credentials by action and resource.
control.register(controlPlane({
credentials: [
{
token: process.env.DISCOVERY_TOKEN,
scopes: ['meta:read', 'services:read'],
services: ['users', 'payments']
},
{
token: process.env.CONFIG_TOKEN,
scopes: ['config:read', 'config:write'],
namespaces: ['users']
}
]
}));A valid credential without the required scope or resource grant receives 403; an unknown credential receives 401.
const current = await client.getConfig('users');
await client.setConfig(
'users',
{ greeting: 'Hello' },
{
expectedRevision: current.revision,
expectedEpoch: current.epoch
}
);
const config = await client.watchConfig('users', {
validate(values) {
if (typeof values.greeting !== 'string') {
throw new Error('greeting must be a string');
}
}
});Updates replace the whole namespace and reject stale writes. Invalid or unavailable updates preserve the last accepted snapshot.
OpenMesh emits exporter-neutral lifecycle events only when observers are enabled.
import { metrics } from '@opentelemetry/api';
import { createOpenTelemetryObservers } from 'openmesh-node/otel';
const telemetry = createOpenTelemetryObservers({
meter: metrics.getMeter('users-service'),
attributes: {
service: 'users',
region: 'nz'
}
});
const app = openmesh({
onEvent: telemetry.onAppEvent
});The bridge records bounded, low-cardinality request/peer/admission/concurrency metrics and does not emit raw request paths, peer URLs, bodies, headers, config values, or tokens.
OpenMesh can host existing Express/Fastify components without pretending they are native OpenMesh middleware.
app.useExpress(cors());
app.mount('/legacy', expressApp);
app.fastify('/validated', async host => {
host.get('/hello', async () => ({
engine: 'fastify'
}));
});Those integrations are optional. The native request path does not require them.
| Import | Purpose |
|---|---|
openmesh-node |
Typed HTTP runtime + app.mesh() facade |
openmesh-node/plugins |
Native plugins and body parsers |
openmesh-node/http |
Advanced functional contract API |
openmesh-node/db |
ORM/database resource lifecycle, health and transaction adapters |
openmesh-node/mesh |
Low-level peer routing / client data plane |
openmesh-node/services |
Registration, discovery, config, control plane |
openmesh-node/services/redis |
Durable Redis adapters |
openmesh-node/services/testing |
Adapter conformance harness |
openmesh-node/otel |
OpenTelemetry-compatible metrics bridge |
Every published subpath is exercised through real packed-tarball CJS + ESM smoke tests in CI.
OpenMesh does not market itself as “the fastest Node.js framework.”
The primary 0.5 benchmark measures a complete service call with the same Node caller and target while changing only the data plane. On the recorded Intel macOS run, median throughput was 1,807 req/s for OpenMesh, 1,281 req/s for a two-proxy Envoy path, and 1,243 req/s for a two-sidecar Dapr path. OpenMesh also used less summed CPU than either sidecar topology in that run.
The point is architectural, not language bragging:
How much does the service-mesh data path cost after the application has decided to make a service call?
The older OpenMesh/Fastify benchmark remains in CI as an internal native-HTTP regression guard. It is no longer the headline comparison.
Method, raw rounds, limits, and reproducibility
git clone https://github.com/yaohuangguan/openmesh-node.git
cd openmesh-node
npm ci --ignore-scripts
npm run demo:servicesThe demo starts:
control plane
│
├── users-a
├── users-b
│
└── gateway
It then changes configuration live, routes through discovery, shuts down the selected instance, and verifies traffic moves to the remaining service.
OpenMesh is:
- a Node.js service runtime;
- an application-level data plane for known HTTP services;
- a discovery/configuration control plane with pluggable storage;
- a place to make retry, overload, streaming, and shutdown semantics explicit.
OpenMesh is not:
- a Kubernetes replacement;
- a distributed consensus system;
- a NAT traversal layer or DHT;
- a hidden sidecar mesh;
- a claim that every application should stop using Express or Fastify.
The project is pre-1.0. Public APIs are usable, but minor versions can still evolve compatibility.
npm ci --ignore-scripts
npm test
npm run test:types
npm run test:package
npm run demo:services
npm run benchCI covers Node 22/24 on Linux and Windows, real Redis integration, packed-package imports, examples, and normalized benchmark regression.
Website Docs · 0.5 Architecture · Application-native Mesh · API · Advanced HTTP Contracts · Databases & ORMs · Services & control plane · Peer routing · Observability · Plugins · Performance · Releasing · Changelog
Bug reports, production edge cases, benchmark reproductions, adapter implementations, and focused PRs are welcome.
See CONTRIBUTING.md.