Skip to content

@connectum/core

The central package of the Connectum framework. Provides createServer() -- a factory function that creates a production-ready gRPC/ConnectRPC server with explicit lifecycle control, protocol plugin system, graceful shutdown, and TLS support.

Layer: 0 (Server Foundation) -- zero internal dependencies

Related Guides

Full API Reference

Complete TypeScript API documentation: API Reference

Installation

bash
pnpm add @connectum/core

Requires: Node.js >= 22.13.0 (packages ship compiled .js + .d.ts + source maps)

Quick Start

typescript
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import routes from '#gen/routes.js';

const server = createServer({
  services: [routes],
  port: 5000,
  protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
  shutdown: { autoShutdown: true },
});

server.on('ready', () => {
  healthcheckManager.update(ServingStatus.SERVING);
  console.log(`Server ready on port ${server.address?.port}`);
});

server.on('error', (err) => console.error(err));

await server.start();

API Reference

createServer(options)

Factory function that creates an unstarted Server instance.

typescript
function createServer(options: CreateServerOptions): Server;

The server is created in CREATED state. Call server.start() to begin accepting connections.

CreateServerOptions

OptionTypeDefaultDescription
servicesServiceDefinition[](required)Service definitions to register on the server (created with defineService / defineLazyService)
portnumber5000Server port
hoststring"0.0.0.0"Server host to bind
tlsTLSOptionsundefinedTLS configuration for secure connections
protocolsProtocolRegistration[][]Protocol plugins (healthcheck, reflection, custom)
shutdownShutdownOptions{}Graceful shutdown configuration
interceptorsInterceptor[][]ConnectRPC interceptors. When omitted or [], no interceptors are applied. Use createDefaultInterceptors() from @connectum/interceptors for the production-ready chain.
allowHTTP1booleantrueAllow HTTP/1.1 connections. Without TLS the default server is plaintext HTTP/1.1; set false for h2c. See the transport matrix
handshakeTimeoutnumber30000Handshake timeout in milliseconds
eventBusEventBusLikeundefinedEvent bus for lifecycle management
http2OptionsSecureServerOptionsundefinedAdditional HTTP/2 server options
jsonOptionsPartial<JsonReadOptions & JsonWriteOptions>undefinedConnect JSON serialization options applied server-wide. See JSON serialization.
transportValidation"error" | "warn" | "off""error"Startup validation: bidi-streaming methods on a plaintext HTTP/1.1 transport fail fast with CONNECTUM_UNSUPPORTED_STREAMING_TRANSPORT instead of hanging at runtime. See the transport matrix
catalogServiceCatalogundefinedFull service registry (typeName → DescService, typically the generated serviceCatalog). Drives startup validation and remote routing. A pure local monolith needs none of the catalog options. See Service Catalog.
enabledServicesreadonly string[]undefinedProto typeNames to mount locally from services; any service not listed is treated as remote (resolved via remoteResolver). undefined mounts every provided service locally.
remoteResolverRemoteResolverundefinedResolves a non-local service to a Transport for server.client() / ctx.call. Synchronous, no network I/O. See Resolvers.
outgoingInterceptorsInterceptor[]undefinedClient-side interceptors applied to every outgoing server.client() / ctx.call.
propagateHeadersreadonly string[][]Inbound header names copied onto every outgoing ctx.call / ctx.stream. Empty by default; use defaultPropagateHeaders (W3C trace-context) as a base, e.g. [...defaultPropagateHeaders, "x-tenant-id"].

JSON serialization

By default, Connect omits fields with implicit presence from JSON responses (proto3 scalar 0, empty string, empty list, enum default). Set jsonOptions to change this server-wide -- it is passed to the underlying connectNodeAdapter, so it also applies to framework-registered protocol services (healthcheck, reflection).

typescript
const server = createServer({
  services: [routes],
  // Include zero/default fields in JSON responses instead of omitting them.
  jsonOptions: { alwaysEmitImplicit: true },
});

protobuf-es v2 field name

The relevant JsonWriteOptions field is alwaysEmitImplicit (it was named emitDefaultValues in protobuf-es v1, which does not apply to Connectum).

Server Interface

Extends EventEmitter. Provides explicit lifecycle control.

Lifecycle Methods

typescript
interface Server extends EventEmitter {
  /** Start the server. Throws if not in CREATED state. */
  start(): Promise<void>;

  /** Stop the server gracefully. Throws if not in RUNNING state. */
  stop(): Promise<void>;
}

State Properties

typescript
interface Server {
  /** Current server address (null until started) */
  readonly address: AddressInfo | null;

  /** Whether server is currently running */
  readonly isRunning: boolean;

  /** Current server state */
  readonly state: ServerState;

  /** Underlying HTTP server (HTTP/1.1 or HTTP/2 depending on TLS / allowHTTP1 config; null until started) */
  readonly transport: Http2SecureServer | Http2Server | HttpServer | null;

  /** Registered service definitions */
  readonly routes: ReadonlyArray<ServiceDefinition>;

  /** Registered interceptors */
  readonly interceptors: ReadonlyArray<Interceptor>;

  /** Registered protocols */
  readonly protocols: ReadonlyArray<ProtocolRegistration>;

  /** Event bus instance (null if not configured) */
  readonly eventBus: EventBusLike | null;
}

Runtime Operations (before start())

typescript
interface Server {
  /** Add a service definition. Throws if the server is already started, or if routes have already been materialized via local-transport access (e.g. a prior `server.localClient()`, `server.client()`, or `server.hasService()` call). Add services/interceptors/protocols before any local-transport access. */
  addService(service: ServiceDefinition): void;

  /** Add an interceptor. Throws if the server is already started, or if routes have already been materialized via local-transport access (e.g. a prior `server.localClient()`, `server.client()`, or `server.hasService()` call). Add services/interceptors/protocols before any local-transport access. */
  addInterceptor(interceptor: Interceptor): void;

  /** Add a protocol. Throws if the server is already started, or if routes have already been materialized via local-transport access (e.g. a prior `server.localClient()`, `server.client()`, or `server.hasService()` call). Add services/interceptors/protocols before any local-transport access. */
  addProtocol(protocol: ProtocolRegistration): void;
}

Shutdown Hooks

typescript
interface Server {
  /** Register an anonymous shutdown hook */
  onShutdown(handler: ShutdownHook): void;

  /** Register a named shutdown hook */
  onShutdown(name: string, handler: ShutdownHook): void;

  /** Register a named shutdown hook with dependencies */
  onShutdown(name: string, dependencies: string[], handler: ShutdownHook): void;

  /** AbortSignal that is aborted when server begins shutdown */
  readonly shutdownSignal: AbortSignal;
}

In-Process Transport

@connectum/core ships a built-in in-process transport that lets you call locally registered services as direct function invocations — no HTTP/2, TLS, sockets, or wire serialization — while preserving 1-to-1 behavioural parity with the HTTP path (interceptors, validation, authorization, error mapping, streaming semantics, OpenTelemetry spans and metrics).

typescript
import { createServer, createLocalTransport, defineService } from '@connectum/core';
import { GreeterService } from './gen/greeter_pb.js';

const greeterService = defineService(GreeterService, {
  async sayHello(req, ctx) {
    return { message: `Hello, ${req.name}!` };
  },
});

const server = createServer({ services: [greeterService] });

// Auto-routing client: in-process if `GreeterService` is registered on
// this server, else via the configured `remoteResolver`, else throws
// `CatalogConfigError` at construction.
const greeter = server.client(GreeterService);
await greeter.sayHello({ name: 'world' }); // no server.start() needed

// Low-level helpers:
const localOnly = server.localClient(GreeterService);
const transport = createLocalTransport(server, { interceptors: [/* client-side */] });
const isRegistered = server.hasService(GreeterService);
APIDescription
server.client(service, options?)Auto-routing factory: local if registered, else via the configured remoteResolver; if neither, fail-fast CatalogConfigError at construction. A resolver returning null also fails at construction with ConnectError(Code.Unavailable) — the resolver runs inside server.client(), before any RPC is invoked.
server.localClient(service)Low-level helper that always returns an in-process client.
server.hasService(desc)Synchronous registry lookup by desc.typeName.
createLocalTransport(server, options?)Returns a ConnectRPC Transport bound to the server's router; supports client-side interceptors.

The in-process transport is available immediately after createServer({...})server.start() is not required. A single Server instance can serve HTTP and in-process clients concurrently.

See the dedicated guide

In-Process Transport — motivation, polyglot deployment pattern, observability parity, limitations.

createCatalogClient(options)

Available since 1.1.0.

A standalone, catalog-typed client that exposes the same typed call (unary) and stream (server/client/bidi) surface as the in-handler ctx.call / ctx.stream, but usable outside a Server — in a Temporal worker, a scheduler, or a CLI — without constructing a server.

typescript
function createCatalogClient(options: CreateCatalogClientOptions): CatalogClient;
OptionTypeDefaultDescription
catalogServiceCatalog(required)The service catalog backing typed dispatch — the same object passed to createServer({ catalog }).
resolverRemoteResolver(required)Resolves every target's transport (singleTransportResolver / mapResolver / dnsResolver / perServiceEnvResolver).

Returns a CatalogClient{ call, stream }, keyed off the generated ConnectumCallMap / ConnectumStreamMap, so calls are statically checked exactly as on the handler ctx.

typescript
import { createCatalogClient, mapResolver } from '@connectum/core';
import { createGrpcTransport } from '@connectrpc/connect-node';
import { serviceCatalog } from '#gen/catalog.js'; // from @connectum/protoc-gen-catalog

const client = createCatalogClient({
  catalog: serviceCatalog,
  resolver: mapResolver({
    'trip.v1.TripService': createGrpcTransport({ baseUrl: process.env.TRIP_ADDR ?? 'http://localhost:8080' }),
  }),
});

// Fully typed off the generated catalog — same surface as ctx.call:
const trip = await client.call('trip.v1.TripService/StartTrip', { vehicleId: 'veh-42' });

Every target is routed through the supplied RemoteResolver, and the resolved transport is cached per (typeName, endpoint). There is no in-process/local path: a service the resolver cannot resolve fails with Code.Unavailable. The rest of the error model mirrors ctx.call — an unknown service/method (or wrong method kind) fails with Code.Unimplemented, and a resolver that throws surfaces as Code.Internal (cause preserved).

No inbound request to cascade from

Unlike ctx.call, there is no inbound request, so CallOptions are applied verbatim: the signal / timeoutMs are not cascaded or clamped, no inbound headers are propagated, and no ContextValues are forwarded.

See Service Catalog for the catalog model and resolver wiring.

ServerState

typescript
const ServerState = {
  CREATED: 'created',
  STARTING: 'starting',
  RUNNING: 'running',
  STOPPING: 'stopping',
  STOPPED: 'stopped',
} as const;

LifecycleEvent

typescript
const LifecycleEvent = {
  START: 'start',     // Emitted when server starts (before ready)
  READY: 'ready',     // Emitted when server is ready to accept connections
  STOPPING: 'stopping', // Emitted when server begins graceful shutdown
  STOP: 'stop',       // Emitted when server stops
  ERROR: 'error',     // Emitted on error
} as const;

ShutdownOptions

OptionTypeDefaultDescription
timeoutnumber30000Timeout in ms for graceful shutdown
signalsNodeJS.Signals[]["SIGTERM", "SIGINT"]Signals to listen for
autoShutdownbooleanfalseEnable automatic graceful shutdown on signals
forceCloseOnTimeoutbooleantrueForce close HTTP/2 sessions when timeout exceeded

Protocol Plugin System

Protocols implement the ProtocolRegistration interface to register themselves on the server.

typescript
interface ProtocolRegistration {
  readonly name: string;
  register(router: ConnectRouter, context: ProtocolContext): void;
  httpHandler?: HttpHandler;
}

interface ProtocolContext {
  readonly registry: ReadonlyArray<DescFile>;
}

TLS Configuration

typescript
interface TLSOptions {
  keyPath?: string;   // Path to TLS key file
  certPath?: string;  // Path to TLS certificate file
  dirPath?: string;   // Directory with server.key and server.crt
}

Utility functions:

typescript
function getTLSPath(): string;
function readTLSCertificates(options?: TLSOptions): { key: Buffer; cert: Buffer };

Environment Configuration (@connectum/core/config)

Type-safe environment configuration using Zod schemas (12-Factor App).

typescript
import { parseEnvConfig, safeParseEnvConfig } from '@connectum/core/config';

const config = parseEnvConfig(); // throws on invalid config
const result = safeParseEnvConfig(); // returns { success, data/error }
Environment VariableTypeDefaultDescription
PORTnumber5000Server port
LISTENstring"0.0.0.0"Listen address
LOG_LEVEL"debug" | "info" | "warn" | "error""info"Log level
LOG_FORMAT"json" | "pretty""json"Log format
LOG_BACKEND"otel" | "pino" | "console""otel"Logger backend
NODE_ENV"development" | "production" | "test""development"Node environment
HTTP_HEALTH_ENABLEDbooleanfalseEnable HTTP health endpoints
HTTP_HEALTH_PATHstring"/healthz"HTTP health endpoint path
OTEL_SERVICE_NAMEstring--OpenTelemetry service name
OTEL_EXPORTER_OTLP_ENDPOINTstring (URL)--OTLP exporter endpoint
GRACEFUL_SHUTDOWN_ENABLEDbooleantrueEnable graceful shutdown
GRACEFUL_SHUTDOWN_TIMEOUT_MSnumber30000Shutdown timeout in ms

Server Lifecycle

CREATED ──start()──> STARTING ──> RUNNING ──stop()──> STOPPING ──> STOPPED
                         │                                │
                         └──(error)──> STOPPED            └──(error)──> STOPPED

Events are emitted in this order: start -> ready (success) or error (failure), then stopping -> stop (or error).

Error Protocol

SanitizableError (interface)

Interface for errors that carry rich server-side details while exposing only a safe message to clients. The ErrorHandler interceptor from @connectum/interceptors recognizes this interface and sanitizes errors automatically.

typescript
interface SanitizableError {
  readonly clientMessage: string;
  readonly serverDetails: Readonly<Record<string, unknown>>;
}
PropertyTypeDescription
clientMessagestringSafe message returned to the client
serverDetailsReadonly<Record<string, unknown>>Structured details for server-side logging

isSanitizableError(err)

Type guard that checks whether a value implements the SanitizableError protocol. Returns true only if the value is an Error instance with a clientMessage string, a non-null serverDetails object, and a numeric code property.

typescript
function isSanitizableError(err: unknown): err is Error & SanitizableError & { code: number };
typescript
import { isSanitizableError } from '@connectum/core';

if (isSanitizableError(err)) {
  // err.clientMessage -- safe for the client
  // err.serverDetails -- rich details for logging
  // err.code          -- numeric gRPC status code
}

Published Package Format

All @connectum/* packages are built with tsup and ship:

  • Compiled .js files (ESM) -- ready to run on any ES module-capable runtime (Node.js 22+, Bun, tsx)
  • TypeScript declarations (.d.ts) -- full type information for IDE support and type checking
  • Source maps (.js.map) -- accurate stack traces pointing to the original TypeScript source

No special loader or register hook is needed. All runtimes can import @connectum/* packages directly.

See Runtime Support: Node.js vs Bun vs tsx for details.

Exports Summary

ExportSubpathDescription
createServer.Server factory function
createCatalogClient.Standalone catalog-typed call / stream client usable outside a Server (since 1.1.0)
createLocalTransport.In-process transport factory (guide)
defineService, defineLazyService.Service registration (descriptor + handlers); the services entries
defineCatalog, mergeCatalogs.Service catalog construction (guide)
singleTransportResolver, mapResolver, dnsResolver, perServiceEnvResolver.Remote resolver helpers (guide)
parseServicesEnv, matchServicesPattern, mergeEnabledServices.enabledServices activation helpers
defaultPropagateHeaders.W3C trace-context header allow-list for propagateHeaders
CatalogConfigError.Catalog/resolver misconfiguration error (fail-loud)
ServerState.Server state constants
LifecycleEvent.Lifecycle event name constants
isSanitizableError.Type guard for SanitizableError protocol
getTLSPath, readTLSCertificates, tlsPath.TLS utilities
EventBusLike.Event bus lifecycle interface
CatalogClient, CreateCatalogClientOptions (since 1.1.0), SanitizableError, Server, CreateServerOptions, ShutdownOptions, ServiceDefinition, ServiceOptions, Context, CallOptions, ServiceCatalog, RemoteResolver, etc..TypeScript types
parseEnvConfig, safeParseEnvConfig, schemas./configEnv configuration