Build Your First Connectum Service
Build your first service from a new project to one successful RPC. You will define a proto contract, generate TypeScript, implement a handler, start a Connectum server, and verify both the happy path and request validation.
Outcome: a runnable Greeter service with health checks, reflection, graceful shutdown, error handling, and proto validation. Production capabilities such as TLS, authentication, observability, and resilience are focused next steps rather than prerequisites for your first call.
Prefer a scaffold?
connectum init downloads the version-pinned official getting-started base and can add selected modules. Follow Scaffolding a Service for that route. Continue here when you want to understand each file yourself.
Prerequisites
- Node.js >= 25.2.0 -- native TypeScript via type stripping
- pnpm >= 11 -- follow the pnpm installation instructions
- buf -- installed automatically via
@bufbuild/bufnpm package
- Bun >= 1.3.6 -- TypeScript runs natively, no loader needed
- buf -- installed automatically via
@bufbuild/bufnpm package
Node.js version for consumers
This guide uses Node.js 25+ for native .ts execution of your own source files. However, @connectum/* packages ship compiled JavaScript, so if you compile your own code (e.g., with tsx or a build tool), you can run on Node.js >= 22.13.0. See Runtime Support.
Bun executes your TypeScript sources directly, and @connectum/* packages ship compiled JavaScript, so nothing else is required. See Runtime Compatibility for the current support level.
1. Project Setup
mkdir greeter-service && cd greeter-servicenpm init -ypnpm initbun init -y -mThe Bun form passes -m so bun init writes only package.json and tsconfig.json instead of also scaffolding a sample entry point and README.
Install dependencies:
# Core framework
npm install @connectum/core @connectum/healthcheck @connectum/reflection @connectum/interceptors
# ConnectRPC runtime
npm install @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf
# Validation (recommended: @connectrpc/validate)
npm install @bufbuild/protovalidate @connectrpc/validate
# Dev dependencies (buf + code generation)
npm install -D typescript @types/node @bufbuild/buf @bufbuild/protoc-gen-es# Core framework
pnpm add @connectum/core @connectum/healthcheck @connectum/reflection @connectum/interceptors
# ConnectRPC runtime
pnpm add @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf
# Validation (recommended: @connectrpc/validate)
pnpm add @bufbuild/protovalidate @connectrpc/validate
# Dev dependencies (buf + code generation)
pnpm add -D typescript @types/node @bufbuild/buf @bufbuild/protoc-gen-es# Core framework
bun add @connectum/core @connectum/healthcheck @connectum/reflection @connectum/interceptors
# ConnectRPC runtime
bun add @connectrpc/connect @connectrpc/connect-node @bufbuild/protobuf
# Validation (recommended: @connectrpc/validate)
bun add @bufbuild/protovalidate @connectrpc/validate
# Dev dependencies (buf + code generation)
bun add -d typescript @types/node @bufbuild/buf @bufbuild/protoc-gen-esThe ConnectRPC runtime libraries are peer dependencies of Connectum: the copies you install here are the ones Connectum uses, so your generated code and the framework share one @bufbuild/protobuf. Keep them within @bufbuild/protobuf ^2.16.0 and @connectrpc/connect / @connectrpc/connect-node ^2.2.0 — see Peer dependencies on protobuf and Connect.
Add the following fields to package.json, keeping the dependencies and devDependencies installed above:
{
"name": "greeter-service",
"version": "1.0.0",
"type": "module",
"imports": {
"#gen/*": "./gen/*",
"#*": "./src/*"
},
"scripts": {
"start": "node src/index.ts",
"dev": "node --watch src/index.ts",
"typecheck": "tsc --noEmit",
"build:proto": "buf generate proto"
},
"engines": { "node": ">=25.2.0" }
}{
"name": "greeter-service",
"version": "1.0.0",
"type": "module",
"imports": {
"#gen/*": "./gen/*",
"#*": "./src/*"
},
"scripts": {
"start": "bun src/index.ts",
"dev": "bun --watch src/index.ts",
"typecheck": "bunx tsc --noEmit",
"build:proto": "buf generate proto"
}
}Create tsconfig.json (type checking only -- no compilation):
{
"compilerOptions": {
"noEmit": true,
"target": "esnext",
"module": "nodenext",
"moduleResolution": "nodenext",
"allowImportingTsExtensions": true,
"erasableSyntaxOnly": true,
"verbatimModuleSyntax": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts", "gen/**/*.ts"],
"exclude": ["node_modules"]
}Create project structure:
mkdir -p src/services gen proto2. Proto Definition
Create proto/greeter.proto:
syntax = "proto3";
package greeter.v1;
import "buf/validate/validate.proto";
service GreeterService {
rpc SayHello(SayHelloRequest) returns (SayHelloResponse) {}
}
message SayHelloRequest {
string name = 1 [(buf.validate.field).string.min_len = 1];
}
message SayHelloResponse {
string message = 1;
}Create buf.yaml to declare the validate dependency:
version: v2
modules:
- path: proto
deps:
- buf.build/bufbuild/protovalidateThen fetch dependencies:
npx buf dep updatepnpm exec buf dep updatebunx buf dep update3. Code Generation
Create buf.gen.yaml:
version: v2
plugins:
- local: protoc-gen-es
out: gen
include_imports: true
opt:
- target=ts
- import_extension=.ts
- erasable_syntax=true
inputs:
- directory: protoerasable_syntax=true generates Protobuf enums as as const objects instead of TypeScript enum, which native type stripping and erasableSyntaxOnly reject. It needs @bufbuild/protoc-gen-es and @bufbuild/protobuf 2.13.0 or later.
Run code generation:
npm run build:protopnpm run build:protobun run build:protoThis produces gen/greeter_pb.ts containing message schemas, types, and the service definition. include_imports: true also generates the imported validation descriptors under gen/buf/validate/, which the service module references.
Proto enums and native TypeScript
With erasable_syntax=true, a proto enum is generated as an object: Status.ACTIVE works as usual, but there is no reverse mapping (Status[1]). See Proto Enums.
4. Service Handler
Create src/services/greeterService.ts:
import { create } from '@bufbuild/protobuf';
import { defineService } from '@connectum/core';
import { GreeterService, SayHelloResponseSchema } from '#gen/greeter_pb.ts';
import type { SayHelloRequest } from '#gen/greeter_pb.ts';
export const greeterService = defineService(GreeterService, {
async sayHello(request: SayHelloRequest) {
const name = request.name || 'World';
return create(SayHelloResponseSchema, {
message: `Hello, ${name}!`,
});
},
});5. Server Entry Point
Create src/index.ts:
import { createServer } from '@connectum/core';
import { Healthcheck, healthcheckManager, ServingStatus } from '@connectum/healthcheck';
import { Reflection } from '@connectum/reflection';
import { createDefaultInterceptors } from '@connectum/interceptors';
import { greeterService } from './services/greeterService.ts';
const server = createServer({
services: [greeterService],
port: 5000,
// grpcurl uses HTTP/2; plaintext HTTP/1.1 is the default transport.
allowHTTP1: false,
protocols: [Healthcheck({ httpEnabled: true }), Reflection()],
interceptors: createDefaultInterceptors(),
shutdown: { autoShutdown: true },
});
server.on('ready', () => {
console.log(`Server ready on port ${server.address?.port}`);
healthcheckManager.update(ServingStatus.SERVING);
});
server.on('stopping', () => {
healthcheckManager.update(ServingStatus.NOT_SERVING);
});
server.on('error', (err) => console.error(err));
await server.start();6. Run & Test
# Node.js 25+ (native TypeScript)
pnpm dev
# tsx (Node.js 22+)
npx tsx src/index.tsbun --watch src/index.tsgRPC (grpcurl)
# List services (reflection)
grpcurl -plaintext localhost:5000 list
# Call SayHello
grpcurl -plaintext -d '{"name": "Alice"}' localhost:5000 greeter.v1.GreeterService/SayHello
# Health check
grpcurl -plaintext localhost:5000 grpc.health.v1.Health/CheckConnectRPC over HTTP/1.1
The server above uses plaintext h2c so grpcurl can call it. To use a browser or an HTTP/1.1 client such as curl, remove allowHTTP1: false (or set it to true) and restart the server. The default without TLS is HTTP/1.1.
# Call SayHello via ConnectRPC HTTP/1.1
curl -X POST http://localhost:5000/greeter.v1.GreeterService/SayHello \
-H "Content-Type: application/json" \
-d '{"name": "Bob"}'
# HTTP health endpoint
curl http://localhost:5000/healthzWhat You Get Out of the Box
| Feature | Details |
|---|---|
| Error handling | Automatic error normalization to gRPC status codes |
| Validation | Proto constraint validation via @connectrpc/validate |
| Health checks | gRPC + HTTP endpoints |
| Reflection | Runtime service discovery |
| Graceful shutdown | SIGTERM/SIGINT with connection draining |
Resilience interceptors (timeout, bulkhead, circuit breaker, retry) are opt-in. Add them after the first call works by following the built-in interceptor guide.
7. Verify Validation
Use the h2c server configuration from Step 5 for this grpcurl command. If you switched to HTTP/1.1 above, restore allowHTTP1: false and restart the server first.
The min_len = 1 rule from Step 2 is enforced automatically by the validation interceptor:
grpcurl -plaintext -d '{"name": ""}' localhost:5000 greeter.v1.GreeterService/SayHello
# ERROR: Code: InvalidArgument
# Message: name: must be at least 1 characters [string.min_len]No application code is required: proto constraints are validated before your handler runs. See Validation for constraint setup and failure handling.
Next Steps
Your first service is complete. Choose the next task; none is required to finish this tutorial.
| Goal | Continue with |
|---|---|
| Protect traffic | TLS and mTLS, then authentication and authorization |
| Observe the service | Tracing, metrics, and logging |
| Tune lifecycle behavior | Graceful Shutdown |
| Add resilience deliberately | Built-in Interceptor Chain |
| Sync reflected contracts | Server Reflection and @connectum/cli |
| Call another service | Choosing a Communication Mechanism |
| Test the service | Testing |
| Deploy it | Docker and Kubernetes |
Troubleshooting
- Generation fails: review the
buf.yaml,buf.gen.yaml, and import extension, then see TypeScript and proto generation. - The server starts but calls fail: confirm port, plaintext/TLS mode, and the fully-qualified service name with Server Reflection.
- Node.js or Bun behaves differently: use the canonical Runtime Compatibility matrix.