Documentation/Learn/Quickstart
In this section

Your first service

Build a small service that reports its status, expose it over HTTP, and call it from a separate process. You need Node.js 22+ and a package manager. No database, Docker or supervisor is required.

1. Create the project

mkdir signal-service
cd signal-service
pnpm init
pnpm add @omnitron-dev/titan @omnitron-dev/netron-browser
pnpm add -D typescript @types/node
mkdir src

Set "type": "module" in package.json. Keep the installed versions in your lockfile. Create this compiler configuration:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "skipLibCheck": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": false
  },
  "include": ["src/**/*.ts"]
}

Compile with tsc so legacy decorators and their metadata are emitted. Node's built-in TypeScript stripping does not compile those decorators.

2. Declare the service

import { Service, Public } from '@omnitron-dev/titan/decorators';

@Service('signals@1.0.0')
export class SignalsService {
  @Public()
  async read(): Promise<{ status: string }> {
    return { status: 'online' };
  }
}

@Service gives the service its wire identity. @Public() exposes the method to RPC callers. This example has no authentication and accepts no arguments; it is intended to run on your local machine.

3. Register the module and transport

import { Application, Module } from '@omnitron-dev/titan';
import { HttpTransport } from '@omnitron-dev/titan/netron/transport/http';
import { SignalsService } from './signals.service.js';

@Module({ providers: [SignalsService] })
class AppModule {}

const app = await Application.create(AppModule);
const netron = app.netron;
if (!netron) throw new Error('Netron is unavailable');

netron.registerTransport('http', () => new HttpTransport());
netron.registerTransportServer('http', {
  name: 'api',
  options: { host: '127.0.0.1', port: 3000 },
});

await app.start();
console.log('Signals service ready at http://127.0.0.1:3000');

Register transports before app.start(). Registration stores the server configuration; startup opens the listener and exposes service instances. The application installs graceful shutdown handlers by default.

Compile and run:

pnpm exec tsc
node dist/main.js

Keep this terminal running. The readiness message appears after startup completes. Stop the process with Ctrl+C when finished.

4. Make a typed call

import { createClient } from '@omnitron-dev/netron-browser';
import type { SignalsService } from './signals.service.js';

const client = createClient({ url: 'http://127.0.0.1:3000' });
try {
  await client.connect();
  const signals = client.service<SignalsService>('signals@1.0.0');
  console.log(await signals.read());
} finally {
  await client.disconnect();
}

In a second terminal, from the same project directory:

pnpm exec tsc
node dist/client.js

Expected output:

{ status: 'online' }

The client imports only the service type; the backend implementation is not included in the client at runtime.

If it fails

SymptomCheck
EADDRINUSEAnother process owns port 3000. Choose a free port in both files.
Connection refusedStart the server first and confirm its readiness message.
Service not foundMatch signals@1.0.0 exactly and register the class as a provider.
Decorator compilation errorUse the configuration above and compile with tsc.
Netron is unavailableKeep Titan's core modules enabled for this example.

Extend the service

Add validation before accepting untrusted inputs and authentication before exposing protected methods. Types alone enforce neither boundary.

Continue with the application lifecycle, transport reference, or React bindings.