totoms
v2.5.0
Published
Toto Microservice SDK for NodeJS - A fast, cloud-agnostic, opinionated framework for building microservices in the Toto Ecosystem
Maintainers
Readme
Toto Microservice SDK - NodeJS
The Toto Microservice SDK is a framework for building cloud-agnostic microservices. This is the NodeJS SDK documentation.
Table of Contents
Other:
1. Installation
npm install totomsCloud-Specific Dependencies
Install the peer dependencies for your target cloud platform:
AWS:
npm install @aws-sdk/client-secrets-manager @aws-sdk/client-sns @aws-sdk/client-sqsGCP:
npm install @google-cloud/pubsub @google-cloud/secret-manager2. Overview
Everything starts with TotoMicroservice and the TotoMicroserviceConfiguration.
TotoMicroservice is the main orchestrator that coordinates your entire microservice. It initializes and manages:
- API Controller & API Endpoints: Express-based REST API setup with automatic endpoint registration
- Message Bus & Message Handlers: Event-driven communication via Pub/Sub and Queues. Registration and routing of event handlers to appropriate topics.
- Secrets Management: Automatic loading of secrets from your cloud provider
- Service Lifecycle: Initialization, startup, and shutdown management
The configuration is declarative. The goal is to make it very simple to configure a full microservice, with a syntax that will look like this:
import { getHyperscalerConfiguration, SupportedHyperscalers, TotoMicroservice, TotoMicroserviceConfiguration } from 'totoms';
import { ControllerConfig } from "./Config";
import { SayHello } from './dlg/ExampleDelegate';
const config: TotoMicroserviceConfiguration = {
serviceName: "toto-ms-ex1",
basePath: '/ex1',
environment: {
hyperscaler: process.env.HYPERSCALER as SupportedHyperscalers || "aws",
hyperscalerConfiguration: getHyperscalerConfiguration()
},
customConfiguration: ControllerConfig,
apiConfiguration: {
apiEndpoints: [
{ method: 'GET', path: '/hello', delegate: SayHello }
],
apiOptions: { noCorrelationId: true }
},
};
TotoMicroservice.init(config).then(microservice => {
microservice.start();
});A few things you should pay attention to:
ControllerConfig- that's your custom configuration class, that you can use to do any type of custom initialization and work (e.g. loading secrets). You can find more details in this section
The TotoMicroserviceConfiguration object specifies:
- Service Metadata: Service name and base path for API endpoints
- Environment: Cloud provider (AWS, GCP, Azure) information
- API Configuration: REST endpoints with their handlers
- Message Bus Configuration: Topics to subscribe to and message handlers
- Custom Configuration: Your application-specific settings
3. Usage
3.1. The Toto Microservice Configuration
The microservice is configured through the TotoMicroserviceConfiguration object and the TotoControllerConfig base class.
As seen above, you need to define a Custom Configuration Class that extends the TotoControllerConfig base class as shown below here.
import { TotoControllerConfig } from 'totoms';
export class ControllerConfig extends TotoControllerConfig {
getMongoSecretNames(): { userSecretName: string; pwdSecretName: string; } | null {
return null;
}
getProps(): APIOptions {
return {}
}
}Some things to note:
- The
getMongoSecretNames()method allows you to define the name of the Secrets containing user and pswd of your Mongo DB, if you choose to use it (stored in the Cloud Secrets Manager, depending on the cloud you're deploying to). - The
getProps()method allows you to do some overrides (e.g. no authentication for this service). You can explore the properties, they're well documented in the SDK.
3.2. Create and Register APIs
Your microservice exposes REST API endpoints using Express. Endpoints are defined when creating the API controller and are automatically set up.
Create a Toto Delegate
Every endpoint needs to be managed by a Toto Delegate.
Toto Delegates extend the TotoDelegate abstract class.
This is how you define a Toto Delegate. The following example shows a delegate that processes user creation.
import { TotoDelegate, UserContext, ValidationError, TotoRequest } from 'totoms';
import { Request } from 'express';
class CreateUserDelegate extends TotoDelegate<CreateUserRequest, CreateUserResponse> {
async do(req: CreateUserRequest, userContext: UserContext): Promise<CreateUserResponse> {
// Extract data from the request (already validated)
const { name, email } = req;
// Your business logic here
...
// Return the response
return {
id: newUserId,
name: name,
email: email
};
}
public parseRequest(req: Request): CreateUserRequest {
// Validate and parse the incoming Express request
if (!req.body.name) throw new ValidationError(400, "Name is required");
if (!req.body.email) throw new ValidationError(400, "Email is required");
return {
name: req.body.name,
email: req.body.email
};
}
}
interface CreateUserRequest extends TotoRequest {
name: string;
email: string;
}
interface CreateUserResponse {
id: string;
name: string;
email: string;
}Register Your Delegate
You can now register your delegate with its endpoint (path, route) in the TotoMicroserviceConfiguration object that we saw earlier.
const config: TotoMicroserviceConfiguration = {
serviceName: "toto-ms-ex1",
basePath: '/ex1',
environment: ...,
...
apiConfiguration: {
apiEndpoints: [
{ method: 'POST', path: '/users', delegate: CreateUserDelegate }
]
},
};Exposing the OpenAPI Spec through Swagger UI
If you have an OpenAPI Spec defined for your microservice, you can expose it through Swagger UI.
To do that, you need to add the following to your TotoMicroserviceConfiguration, in the apiConfiguration section:
apiConfiguration: {
...
openAPISpecification: { localSpecsFilePath: './openapi.yaml' }
},After this the API Documentation will be available at: http(s)://<your-microservice-host>:<port>/<base-path>/apidocs
IMPORTANT - API Documentation is ALSO available as a JSON endpoint at: http(s)://<your-microservice-host>:<port>/<base-path>/jsondocs
3.3. Use a Message Bus
The Message Bus enables event-driven communication between microservices. It supports both PUSH (webhook-based from cloud Pub/Sub) and PULL (polling) delivery models, depending on your cloud provider and configuration.
3.3.1. React to Messages
Message handlers are the primary way to react to events.
Create a Message Handler
Create a handler by extending TotoMessageHandler and implementing the required methods:
import { TotoMessageHandler, TotoMessage, ProcessingResponse } from 'totoms';
class TopicRefreshedEventHandler extends TotoMessageHandler {
getHandledMessageType(): string {
// Return the message type this handler processes
return "topicRefreshed";
}
async processMessage(message: TotoMessage): Promise<ProcessingResponse> {
// Access message metadata
const correlationId = message.correlationId;
const messageId = message.id;
// Extract event data
const topicName = message.payload.name;
const blogUrl = message.payload.blogURL;
const user = message.payload.user;
// Your handler has access to context
this.logger.compute(correlationId, `Processing topic refresh for: ${topicName}`);
// Perform your business logic
await this.refreshTopic(topicName, blogUrl, user);
// Return success or failure
return { success: true };
}
private async refreshTopic(name: string, url: string, user: string) {
// Implementation here
}
}Register a Message Handler
Register your message handlers with the message bus configuration.
IMPORTANT NOTE:
- When using PubSub infrastructure, you need to register topics.
Topics are registered by giving them:
- A
logical namewhich is the name that will be used in the application to reference the topic. - A topic identifier (e.g., ARN on AWS or fully-qualified Topic Name on GCP)
- A
import { TotoMessageBus, MessageHandlerRegistrationOptions } from 'totoms';
const messageBus = new TotoMessageBus(config, environment);
// Register topics
messageBus.registerTopic({
logicalName: "topic-events",
topicName: process.env.TOPIC_EVENTS_TOPIC_NAME! // From environment or secrets
});
// Register message handlers
const handlerOptions: MessageHandlerRegistrationOptions = {
topic: { logicalName: "topic-events" }
};
messageBus.registerMessageHandler(
new TopicRefreshedEventHandler(),
handlerOptions
);When the microservice starts, it automatically subscribes to the configured topics and routes incoming messages to the appropriate handlers based on their message type.
3.3.2. Publish Messages
You can always publish messages to topics.
NOTE:
- In the Message Destination, the topic is the logical name of the topic (see above).
import { TotoMessage, MessageDestination } from 'totoms';
async function publishTopicUpdate(messageBus: any, topicId: string, topicName: string) {
// Create the message
const message = new TotoMessage({
type: "topicUpdated",
correlationId: "correlation-id-123",
id: topicId,
payload: {
name: topicName,
timestamp: new Date().toISOString()
}
});
const destination: MessageDestination = {
topicName: "topic-events"
};
await messageBus.publishMessage(destination, message);
}Getting Access to the Message Bus
There are different ways to get access to the Message Bus instance:
Through the
TotoMicroservicesingleton:TotoMicroservice.getInstance().messageBusThrough an existing instance of
TotoMicroserviceIn a
TotoMessageHandleryou will havemessageBusas an instance variable:this.messageBusIn a
TotoDelegate, you can access it through the config or by maintaining a reference in your application
3.4. Expose MCP Tools
The SDK now supports exposing delegates as Model Context Protocol (MCP) Tools. This allows your microservice to be consumed by AI agents and other MCP-compatible clients.
Creating an MCP-Enabled Delegate
To expose a delegate as an MCP tool, extend TotoMCPDelegate instead of TotoDelegate and implement the getToolDefinition() method:
import { TotoMCPDelegate, UserContext, TotoRequest } from 'totoms';
import { TotoMCPToolDefinition } from 'totoms';
import { Request } from 'express';
import z from 'zod';
export class GetTopics extends TotoMCPDelegate<GetTopicsRequest, GetTopicsResponse> {
public getToolDefinition(): TotoMCPToolDefinition {
return {
name: "getTopics",
title: "Get user's topics in Tome",
description: "Retrieves all topics associated with the authenticated user.",
inputSchema: z.object({}) // Define the input schema using Zod
}
}
async do(req: GetTopicsRequest, userContext: UserContext): Promise<GetTopicsResponse> {
const user = userContext.email;
// Your business logic here
const topics = await this.fetchTopicsForUser(user);
return { topics };
}
public parseRequest(req: Request): GetTopicsRequest {
// Parse Express request (for REST API usage)
return {};
}
private async fetchTopicsForUser(user: string) {
// Implementation
...
}
}
interface GetTopicsRequest extends TotoRequest {}
interface GetTopicsResponse {
topics: any[];
}Registering MCP Tools
Register your MCP-enabled delegates in the mcpConfiguration section of your microservice configuration:
import { TotoMicroservice, TotoMicroserviceConfiguration } from 'totoms';
import { GetTopics } from './dlg/GetTopics';
import { GetTopic } from './dlg/GetTopic';
const config: TotoMicroserviceConfiguration = {
serviceName: "tome-ms-topics",
basePath: '/tometopics',
environment: ...,
apiConfiguration: {
apiEndpoints: [
{ method: 'GET', path: '/topics', delegate: GetTopics }
]
},
mcpConfiguration: {
enableMCP: true,
serverConfiguration: {
name: "Tome Topics MCP Server",
tools: [
GetTopics,
GetTopic
]
}
}
};MCP Server Endpoint
When MCP is enabled, the microservice exposes the MCP server on the following path:
POST {basePath}/mcpFor example, a service with basePath: '/tometopics' exposes the MCP server at:
POST /tometopics/mcpThe MCP server uses the Streamable HTTP transport in stateless mode (no session management). To configure an MCP client (e.g. Claude Desktop, VS Code Copilot, or any MCP-compatible client), point it to this endpoint:
{
"mcpServers": {
"tome-topics": {
"type": "http",
"url": "https://<your-host>/tometopics/mcp"
}
}
}Note: Replace
<your-host>andtometopicswith your actual host andbasePath.
Key Points
- Dual Purpose: Delegates extending
TotoMCPDelegatecan serve both as REST API endpoints AND as MCP tools - Tool Definition: The
getToolDefinition()method defines how the tool appears to MCP clients - Input Schema: Use Zod schemas to define and validate tool inputs
- Custom Processing: Override
processToolRequest()if you need custom logic for MCP tool invocations that differs from REST API handling
3.5. Load Secrets
The SDK handles secret loading from your cloud provider automatically. Access secrets through the configuration or use the SecretsManager directly:
import { SecretsManager } from 'totoms';
const secrets = new SecretsManager({ hyperscaler: "aws" });
// Load a secret by name
const apiKey = await secrets.getSecret("api-key");
const databaseUrl = await secrets.getSecret("database-url");Secrets are typically stored as environment variable names or secret manager references, depending on your deployment environment.
3.6. Custom Configurations
You can define your own custom configurations by extending the TotoControllerConfig base class.
An example:
import { TotoControllerConfig } from 'totoms';
export class MyServiceConfig extends TotoControllerConfig {
apiKey: string | undefined;
async load(): Promise<void> {
// Load secrets using the secrets manager
this.apiKey = await this.secretsManager.getSecret("my-api-key");
}
getMongoSecretNames() {
// Return null if your service doesn't use MongoDB
return null;
}
}What you can do with a Custom Configuration:
Load Secrets You can do that by overriding the
load()async method and usingthis.secretsManager.getSecret("your-secret-name")to load secrets.Configure MongoDB Override
getMongoSecretNames(),getDBName(), andgetCollections()to configure MongoDB integration.Custom Authentication Override
getCustomAuthVerifier()to provide custom authentication logic.
3.7. Publish an Agent
The SDK supports publishing Gale Agents — AI agents that can participate in conversations brokered by Gale Broker. An agent receives a user message, processes it (potentially using an LLM), and returns a response. It can also publish intermediate messages back to the conversation in real time while processing.
Create a Conversational Agent
Extend GaleConversationalAgent and implement two required methods:
getManifest()— returns the agent's identity informationonMessage()— handles an incoming conversation message and returns the response
import { AgentConversationMessage, GaleConversationalAgent, AgentManifest } from "totoms";
import { v4 as uuid } from "uuid";
export class MyAgent extends GaleConversationalAgent {
getManifest(): AgentManifest {
return {
agentType: "conversational",
agentId: "my-agent", // Unique agent identifier
humanFriendlyName: "My Agent", // Display name shown to users
}
}
async onMessage(message: AgentConversationMessage): Promise<AgentConversationMessage> {
const streamId = uuid();
// Optionally publish an intermediate message to the conversation
// while you are still processing, to give the user early feedback.
this.publishMessage({
conversationId: message.conversationId,
messageId: uuid(),
agentId: message.agentId,
message: "Got your message, working on it!",
actor: "agent",
stream: { streamId, sequenceNumber: 1, last: false }
});
// ... run your LLM / business logic here ...
const result = "Here is my answer!";
// Return the final response
return {
conversationId: message.conversationId,
messageId: message.messageId,
agentId: message.agentId,
message: result,
actor: "agent",
stream: { streamId, sequenceNumber: 2, last: true }
};
}
}Key points:
publishMessage()lets the agent stream intermediate messages to the client while still processing. This is useful to acknowledge receipt or provide progress updates before the final answer is ready.- The
streamfield is optional but recommended for conversational agents. Setlast: trueonly on the final message. actorshould always be"agent"for messages sent by the agent.
Register the Agent
Add an agentsConfiguration block to your TotoMicroserviceConfiguration:
import { TotoMicroservice, TotoMicroserviceConfiguration } from "totoms";
import { MyAgent } from "./agent/MyAgent";
const config: TotoMicroserviceConfiguration = {
serviceName: "my-microservice",
environment: { ... },
...
agentsConfiguration: {
agents: [
MyAgent
]
}
};
TotoMicroservice.init(config).then(microservice => {
microservice.start();
});Multiple agents can be registered in the agents array.
Required Environment Variables
To integrate with Gale Broker the following environment variables must be set:
| Variable | Description |
|---|---|
| GALE_BROKER_URL | The base URL of the Gale Broker service (e.g. http://gale-broker:8080/galebroker). Used to register the agent on startup and to publish messages back to conversations. |
| SERVICE_BASE_URL | The publicly reachable base URL of this microservice, including the base path if any (e.g. https://myservice.example.com/basepath). Gale Broker uses this to know where to forward incoming conversation messages for the agent. |
The SDK will throw an error at startup if either variable is not set when any agent is configured.
GCP — Required IAM Roles
If your agent uses Vertex AI (e.g. via @genkit-ai/google-genai), the service account running the microservice must have the following IAM role granted in your Terraform configuration:
resource "google_project_iam_member" "my-service_role_aiplatform" {
project = var.gcp_pid
role = "roles/aiplatform.user"
member = format("serviceAccount:%s", google_service_account.my-service-account.email)
}Without this role the Vertex AI API calls will fail with a 403 Permission Denied error.
Core Components
TotoAPIController
The main controller for building REST APIs with Express. Provides:
- Automatic route registration
- Built-in validation
- CORS support
- Health check endpoints
- File upload support
- API documentation generation
TotoMicroservice
High-level wrapper that initializes the entire microservice stack including API controller, message bus, and environment configuration.
TotoMessageBus
Unified interface for pub/sub messaging across cloud platforms:
- AWS: SNS/SQS
- GCP: Cloud Pub/Sub
- Azure: Service Bus (in development)
TotoControllerConfig
Base configuration class for microservices with support for:
- MongoDB connection management
- Authentication settings
- Secrets management
- Custom validators
Logger
Structured logging with correlation ID support for request tracing.
Validator
Request validation framework with support for:
- JWT token validation
- Google OAuth
- Custom validation logic
Cloud Platform Support
AWS
- Messaging: SNS (topics), SQS (queues)
- Secrets: AWS Secrets Manager
- Region Configuration: Configurable per service
GCP
- Messaging: Cloud Pub/Sub
- Secrets: Secret Manager
- Project Configuration: Uses default project credentials
Azure
- Messaging: Service Bus (in development)
- Secrets: Key Vault (in development)
Local Development & Testing (npm link)
While working on the SDK, you'll often want to test your changes live in a consuming microservice (e.g. toto-ms-xxx) without publishing a new version to npm every time. The simplest way to do this is npm link.
1. Build the SDK and watch for changes
cd toto-microservice-sdk/node
npm run build # build once
npx tsc --watch # keep rebuilding dist/ as you edit2. Register the SDK as a global link
# from toto-microservice-sdk/node
npm link3. Link it into the consuming project
# from the consuming microservice, e.g. gale-dr-dispatcher
npm link totomsThis replaces node_modules/totoms with a symlink to your local SDK checkout. Since tsc --watch keeps dist/ up to date, every change you make in the SDK is picked up immediately by the consuming project — no publish, no reinstall.
4. Undo it when you're done
# from the consuming microservice
npm unlink totoms
npm installThis restores the published version of totoms from the registry.
Note: If
npm linkever misbehaves (e.g. due to dependency duplication), an alternative is to point directly at the local path instead:"totoms": "file:../toto-microservice-sdk/node"in the consumer'spackage.json, followed bynpm install. This is more explicit and portable, but you'll need to re-runnpm installafter each SDK change rather than just relying on the watch/symlink.
License
MIT
Author
nicolasances
Contributing
Contributions are welcome! Please feel free to submit a Pull Request to the toto-microservice-sdk repository.
