Skip to main content

Step 1 — Scaffold

By the end of this step: an empty folder turns into a pnpm workspace with one Titan app saying hello.

Create the monorepo

mkdir my-platform && cd my-platform
git init
echo "node_modules/" > .gitignore
echo "dist/" >> .gitignore
echo ".env" >> .gitignore

Initialise pnpm workspace:

pnpm init

Edit the generated package.json:

{
"name": "my-platform",
"private": true,
"type": "module",
"scripts": {
"dev": "pnpm -F api dev",
"build": "pnpm -r build",
"test": "pnpm -r test"
},
"engines": { "node": ">=22" }
}

Create pnpm-workspace.yaml:

packages:
- 'apps/*'
- 'packages/*'

Create a root tsconfig.json:

{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"isolatedModules": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"useDefineForClassFields": false,
"lib": ["ES2022"],
"resolveJsonModule": true,
"allowImportingTsExtensions": false
}
}

The decorator flags are mandatory for Titan.

Create the first app

mkdir -p apps/api/src
cd apps/api

apps/api/package.json:

{
"name": "api",
"private": true,
"type": "module",
"main": "dist/index.js",
"scripts": {
"dev": "tsx watch src/main.ts",
"build": "tsc -p tsconfig.json",
"start": "node dist/main.js"
},
"dependencies": {
"@omnitron-dev/titan": "latest"
},
"devDependencies": {
"tsx": "latest",
"typescript": "latest"
}
}

apps/api/tsconfig.json:

{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*.ts"]
}

The first Titan app

apps/api/src/main.ts:

import { Application, Module, Injectable, Service, Public } from '@omnitron-dev/titan';

// 1. A trivial service.
@Service('greetings@1.0.0')
class GreetingsService {
@Public()
async hello(name: string): Promise<string> {
return `Hello, ${name}!`;
}
}

// 2. Wire it in a module.
@Module({ providers: [GreetingsService] })
class AppModule {}

// 3. Boot.
const app = await Application.create(AppModule, {
netron: {
http: { port: 3001, host: '0.0.0.0' },
},
});

await app.start();
console.log('api ready on http://localhost:3001');

// Graceful shutdown
process.on('SIGTERM', () => app.stop());
process.on('SIGINT', () => app.stop());

Install + run:

cd ../..
pnpm install
pnpm dev

You should see:

api ready on http://localhost:3001

Verify

In another terminal:

curl -X POST http://localhost:3001/netron \
-H 'Content-Type: application/msgpack' \
-d '...' # MessagePack-encoded RPC call

Easier — use the client we'll build in step 4. For now, install the CLI tool to test:

pnpm add -wD @omnitron-dev/netron-browser

node -e "
const { createClient } = require('@omnitron-dev/netron-browser');
(async () => {
const c = createClient({ url: 'http://localhost:3001' });
await c.connect();
console.log(await c.invoke('greetings', 'hello', ['World']));
})();
"

Output:

Hello, World!

The service signature flowed through: the server hello(name: string): Promise<string> is callable from anywhere that can reach :3001.

What just happened

Three lines you wrote, four things the framework did:

Your lineWhat it triggered
@Service('greetings@1.0.0')Registered the class as a Netron RPC service identified by greetings@1.0.0
@Public()Exposed the method on the wire (default: anonymous-allowed)
@Module({providers: [...]})Registered the class with the DI container
Application.create(AppModule, {netron})Spun up the container, started lifecycle hooks, bound the Netron HTTP listener

The 4-line setup gave you:

  • ✓ Type-safe RPC
  • ✓ MessagePack wire format
  • ✓ DI container
  • ✓ Lifecycle management
  • ✓ Graceful shutdown wiring

Commit

git add .
git commit -m "step 1: scaffold pnpm workspace + first Titan app"

Next

Step 2 — Service → — replace the hello service with a real one backed by a database.

Troubleshooting

SymptomFix
Cannot use import statement outside a moduleEnsure "type": "module" in apps/api/package.json
Decorators are not valid hereCheck experimentalDecorators + emitDecoratorMetadata in tsconfig.json
EADDRINUSE :::3001Another process on 3001; change the port or kill the other process
TypeScript errors on @ServiceRun pnpm install again; ensure @omnitron-dev/titan resolved