Creating Custom Protocol Plugins
Connectum uses a protocol plugin system to extend the server with additional gRPC services and HTTP endpoints. Built-in protocols include Healthcheck and Reflection, but you can create your own.
This is an advanced framework extension point. To expose the standard reflection service, follow Server reflection rather than reimplementing it.
The ProtocolRegistration Interface
Every protocol plugin implements the ProtocolRegistration interface exported from @connectum/core:
interface ProtocolRegistration {
/** Protocol name for identification (e.g. "healthcheck", "reflection") */
readonly name: string;
/** Register protocol services on the router */
register(router: ConnectRouter, context: ProtocolContext): void;
/** Optional HTTP handler for fallback routing (e.g. /healthz endpoint) */
httpHandler?: HttpHandler;
}The ProtocolContext provides access to registered service file descriptors:
interface ProtocolContext {
/** Registered service file descriptors */
readonly registry: ReadonlyArray<DescFile>;
}The optional HttpHandler is called for raw HTTP requests that do not match any ConnectRPC route:
/**
* @returns true if the request was handled, false otherwise
*/
type HttpHandler = (req: Http2ServerRequest, res: Http2ServerResponse) => boolean;How Protocols Are Registered
Protocols are passed to createServer() via the protocols array. During server.start(), each protocol's register() method is called with the ConnectRouter and a context containing all registered service file descriptors:
import { createServer } from '@connectum/core';
import { Healthcheck } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
const server = createServer({
services: [routes],
protocols: [
Healthcheck({ httpEnabled: true }),
Reflection(),
myCustomProtocol, // Your custom protocol
],
});Protocols can also be added before starting the server:
const server = createServer({ services: [routes] });
server.addProtocol(myCustomProtocol);
await server.start();WARNING
Protocols must be added before calling server.start(). Adding a protocol after the server is running will throw an error.
Creating a Custom Protocol
Minimal Example: Service Info Endpoint
A protocol that registers a gRPC service to return metadata about the running server:
import type { ConnectRouter } from '@connectrpc/connect';
import type { ProtocolRegistration, ProtocolContext } from '@connectum/core';
import { InfoService } from '#gen/info_pb.js';
function ServerInfo(): ProtocolRegistration {
const startedAt = new Date().toISOString();
return {
name: 'server-info',
register(router: ConnectRouter, context: ProtocolContext): void {
const serviceNames = context.registry.flatMap(
(file) => file.services.map((s) => s.typeName),
);
router.service(InfoService, {
getInfo: () => ({
startedAt,
serviceCount: serviceNames.length,
services: serviceNames,
}),
});
},
};
}With HTTP Handler
Add a raw HTTP endpoint alongside the gRPC service. The httpHandler function receives HTTP/2 requests that do not match any ConnectRPC route. Return true if you handled the request, false to pass it along:
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
import type { ProtocolRegistration, ProtocolContext } from '@connectum/core';
function CustomHealthEndpoint(): ProtocolRegistration {
const protocol: ProtocolRegistration = {
name: 'custom-health',
register(_router, _context): void {
// No gRPC service needed -- HTTP-only protocol
},
httpHandler(req: Http2ServerRequest, res: Http2ServerResponse): boolean {
if (req.url === '/healthz' && req.method === 'GET') {
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ status: 'ok', timestamp: Date.now() }));
return true;
}
return false;
},
};
return protocol;
}TIP
The built-in Healthcheck protocol already provides HTTP health endpoints at /healthz, /health, and /readyz when httpEnabled: true is set. Use a custom HTTP handler only when you need non-standard behavior.
Example: Prometheus Metrics Endpoint
A protocol that exposes a /metrics HTTP endpoint for Prometheus scraping:
import type { Http2ServerRequest, Http2ServerResponse } from 'node:http2';
import type { ProtocolRegistration } from '@connectum/core';
function Metrics(options: {
path?: string;
collect: () => string;
}): ProtocolRegistration {
const { path = '/metrics', collect } = options;
return {
name: 'prometheus-metrics',
register(): void {
// HTTP-only protocol, no gRPC service registration needed
},
httpHandler(req: Http2ServerRequest, res: Http2ServerResponse): boolean {
if (req.url === path && req.method === 'GET') {
res.writeHead(200, { 'content-type': 'text/plain; version=0.0.4; charset=utf-8' });
res.end(collect());
return true;
}
return false;
},
};
}Usage:
import { createServer } from '@connectum/core';
import { Healthcheck } from '@connectum/healthcheck';
const server = createServer({
services: [routes],
protocols: [
Healthcheck({ httpEnabled: true }),
Metrics({
path: '/metrics',
collect: () => generatePrometheusMetrics(),
}),
],
});Using ProtocolContext
The context.registry field contains an array of DescFile objects (from @bufbuild/protobuf) representing the proto file descriptors of all registered services. This is how the built-in Reflection protocol discovers available services:
register(router, context): void {
// List all registered service type names
for (const file of context.registry) {
for (const service of file.services) {
console.log(`Registered: ${service.typeName}`);
for (const method of service.methods) {
console.log(` - ${method.name} (${method.kind})`);
}
}
}
}Protocol Design Guidelines
Use the factory pattern -- Return
ProtocolRegistrationfrom a function that accepts options. This matches the convention ofHealthcheck()andReflection().Name your protocol -- The
namefield is used for identification and logging. Choose a descriptive, lowercase name.Keep register() synchronous -- The
registermethod signature is synchronous. If you need async setup, do it before creating the protocol or inside the service handlers.Return
falsefrom httpHandler for unmatched routes -- This allows other protocols and the default 404 handler to process the request.Use ProtocolContext for service discovery -- Do not hardcode service names. Use
context.registryto discover what services are available.
Related
- Protocols Overview -- back to overview
- Server Reflection -- built-in reflection protocol
- Custom Interceptors -- creating custom interceptor middleware
- @connectum/core -- Package Guide
- @connectum/core API -- Full API Reference