Cluster Overview
Multi-transport application architecture
What is a Cluster?
A cluster is the recommended way to create JustScale applications. It wraps your app with multi-transport capabilities, allowing the same controllers to serve HTTP requests, CLI commands, and future transports like WebSockets or gRPC.
Instead of creating separate apps for HTTP and CLI, you define routes once and expose them over multiple transports simultaneously.
Creating a Cluster
Use createCluster() to create an application with full cluster capabilities:
import { createCluster } from '@justscale/cluster';
import { UserController } from './controllers/user';
import { DbController } from './controllers/db';
const cluster = createCluster({
controllers: [UserController, DbController],
});
// Start serving - HTTP + cluster socket for CLI
await cluster.serve({ http: 3000 });This single call:
- Creates the underlying JustScale app
- Starts an HTTP server on port 3000 (if @justscale/http is loaded)
- Creates a Unix socket for CLI communication
- Registers all transport handlers automatically
Multi-Transport Controllers
Controllers can define routes for multiple transports. Each transport has its own route factory:
import { createController } from '@justscale/core';
import { Get, Post } from '@justscale/http';
import { Cli } from '@justscale/cli';
import { body } from '@justscale/http/builder';
import { z } from 'zod';
import { UserService } from '../services/user';
const CreateUserArgs = z.object({
email: z.string().email(),
password: z.string().min(8),
});
export const UserController = createController('user', {
inject: { users: UserService },
routes: (services) => ({
// HTTP route - accessible via REST API
create: Post('/')
.apply(body(CreateUserArgs))
.handle(async ({ body, res }) => {
const user = await services.users.create(body);
res.json({ user });
}),
// CLI route - accessible via CLI commands
createCli: Cli('create')
.input(CreateUserArgs)
.handle(async ({ args, io }) => {
const user = await services.users.create(args);
io.log(`Created user: ${user.email}`);
io.result({ id: user.id });
}),
// HTTP route for listing
list: Get('/')
.handle(async ({ res }) => {
const allUsers = await services.users.findAll();
res.json({ users: allUsers });
}),
// CLI route for listing
listCli: Cli('list')
.handle(async ({ io }) => {
const allUsers = await services.users.findAll();
io.table(allUsers, ['id', 'email', 'createdAt']);
}),
}),
});This controller exposes both HTTP and CLI interfaces:
# HTTP usage:
curl -X POST http://localhost:3000/user \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"secret123"}'
curl http://localhost:3000/user
# CLI usage:
justscale user create --email test@example.com --password secret123
justscale user listCluster Methods
cluster.serve()
Start serving all transports. This is the primary way to start your application:
import { createCluster } from '@justscale/cluster';
const cluster = createCluster({ controllers: [] });
// HTTP + cluster socket
await cluster.serve({ http: 3000 });
// Custom socket path
await cluster.serve({
http: 3000,
socketPath: '/tmp/my-app.sock',
});
// Socket only (no HTTP)
await cluster.serve({ noSocket: false });
// HTTP only (no socket - CLI won't work)
await cluster.serve({
http: 3000,
noSocket: true,
});cluster.stop()
Stop the cluster and clean up all resources:
await cluster.stop();cluster.app
Access the underlying JustScale app for testing or direct execution:
import { invoke } from '@justscale/cli';
// Direct command invocation
const result = await invoke(cluster.app, 'user create', {
email: 'test@example.com',
password: 'secret123',
});
// HTTP request simulation (in tests)
const response = await request(cluster.app)
.post('/user')
.send({ email: 'test@example.com', password: 'secret123' });Features with Clusters
Features work seamlessly with clusters and can contribute routes to multiple transports:
import { createCluster } from '@justscale/cluster';
import { AuthFeature } from '@justscale/auth';
import { UserController } from './controllers/user';
const cluster = createCluster({
features: [
AuthFeature(),
],
controllers: [UserController],
});
await cluster.serve({ http: 3000 });The auth feature now provides both HTTP and CLI routes:
# HTTP routes:
curl -X POST http://localhost:3000/auth/register
curl http://localhost:3000/auth/me?token=xxx
# CLI routes:
justscale auth create-user --email admin@example.com
justscale auth list-users
justscale auth delete-session --id abc123Transport Plugins
Transports are registered as plugins. When you import a transport package, it automatically registers itself with the cluster system:
// These imports auto-register their transports:
import '@justscale/http'; // Registers HTTP transport
import '@justscale/cli'; // Registers CLI transport
import { createCluster } from '@justscale/cluster';
import { UserController } from './controllers/user';
// The cluster knows about all registered transports
const cluster = createCluster({
controllers: [UserController],
});
// cluster.serve() starts all registered transports
await cluster.serve({ http: 3000 });Info
Future transports (WebSockets, gRPC, etc.) will follow the same pattern - just import the package and define routes using the transport's route factory.
Cluster Socket
When cluster.serve() is called, a Unix domain socket is created for inter-process communication. This socket:
- Enables the
justscaleCLI to communicate with your running app - Supports bidirectional streaming for progress indicators and prompts
- Uses local credentials for security (only accessible by the same user)
- Auto-cleans up on process exit
await cluster.serve({ http: 3000 });
console.log(`Socket path: ${cluster.socketPath}`);
// Socket path: /tmp/justscale-1000.sock
console.log(`Is serving: ${cluster.isServing}`);
// Is serving: trueConnecting to a Running Cluster
You can connect to a running cluster from another process:
import { connectToCluster } from '@justscale/cluster';
// Connect to the running cluster via Unix socket
const client = await connectToCluster();
// Invoke a CLI command
const result = await client.invoke('user create', {
email: 'test@example.com',
password: 'secret123',
});
// Stream output
client.on('stdout', (data) => console.log(data));
client.on('stderr', (data) => console.error(data));
await client.close();Testing with Clusters
For testing, you can access the underlying app without starting the server:
import { createCluster } from '@justscale/cluster';
import { invoke } from '@justscale/cli';
import { UserController } from './controllers/user';
describe('UserController', () => {
const cluster = createCluster({
controllers: [UserController],
});
test('creates user via CLI', async () => {
const result = await invoke(cluster.app, 'user create', {
email: 'test@example.com',
password: 'secret123',
});
expect(result).toMatchObject({
id: expect.any(String),
});
});
test('creates user via HTTP', async () => {
const response = await request(cluster.app)
.post('/user')
.send({
email: 'test@example.com',
password: 'secret123',
});
expect(response.status).toBe(200);
expect(response.body.user).toMatchObject({
email: 'test@example.com',
});
});
});Comparison: ClusterBuilder vs Cluster
Understanding when to use each:
// createClusterBuilder - composable, type-safe DI
import { createClusterBuilder } from '@justscale/core';
import { UserController } from './controllers/user';
import { UserService } from './services/user';
const app = createClusterBuilder()
.add(UserService)
.add(UserController)
.build()
.compile();
// createCluster - multi-transport serving
import { createCluster } from '@justscale/cluster';
const cluster = createCluster({
controllers: [UserController],
});
await cluster.serve({ http: 3000 }); // HTTP + CLI + future transportsTip
Use createClusterBuilder() for assembling your application with type-safe dependency injection. Use createCluster() for multi-transport serving.