| name | blong-handler |
| description | Create API handlers and library functions in Blong using semantic triple naming (subjectObjectPredicate). Handlers implement business operations, protocol tasks, or reusable logic. Make sure to use this skill whenever you're writing any function in a Blong realm — API endpoints, library helpers, adapter logic, or test steps. Even if the user just says 'add a function' or 'implement this logic in Blong', use this skill. |
Implementing Handlers
Overview
Handlers are functions called by adapters and orchestrators to implement functionality. They follow the framework's conventions for naming, parameter passing, and interoperability.
Purpose
- Business Logic: Implement specific business operations
- API Implementation: Expose business functionality through semantic names
- Reusable Functions: Share common logic via library functions
- Protocol Handling: Implement adapter loop handlers (send, receive, encode, decode)
- Type Safety: Define types for automatic validation
Handler Types
1. Internal Handlers
Predefined handlers for adapter/protocol operations:
send - Prepare data for sending, adapt for protocol
receive - Transform received data, remove protocol details
encode - Convert JavaScript object to Buffer (TCP)
decode - Convert Buffer to JavaScript object (TCP)
exec - Default handler when no specific handler exists
ready - Called when adapter is ready
idleSend - Send keep-alive message
idleReceive - Handle idle timeout
drainSend - Called when send queue is empty
2. API Handlers
Business functionality using semantic triple naming
3. Library Functions
Reusable functions shared between handlers
Naming Convention: Semantic Triples
API handlers use subjectObjectPredicate format:
- subject - namespace/realm name
- object - entity within realm
- predicate - action on entity
Examples
| Handler Name | Subject | Object | Predicate | Purpose |
|---|
userUserAdd | user | user | add | Create a user |
userRoleEdit | user | role | edit | Edit a role |
paymentTransferPrepare | payment | transfer | prepare | Prepare transfer |
mathNumberSum | math | number | sum | Sum numbers |
Realm Structure Example
Realm: user with namespaces identity, permission, user
user/
├── orchestrator/
│ ├── identityDispatch.ts
│ ├── permissionDispatch.ts
│ ├── userDispatch.ts
│ ├── identity/
│ │ ├── identityTokenCreate.ts
│ │ └── identityTokenValidate.ts
│ ├── permission/
│ │ ├── permissionUserCheck.ts
│ │ └── permissionRoleGrant.ts
│ └── user/
│ ├── userUserAdd.ts
│ ├── userUserEdit.ts
│ ├── userUserFind.ts
│ └── userRoleEdit.ts
Handler Pattern
Basic Handler
import {IMeta, handler} from '@feasibleone/blong';
type Handler = ({
param1: string;
param2: number;
}) => Promise<{
result: string;
}>;
export default handler(({
lib: {
helperFunction // Library function
},
errors: { // Domain errors with simplified syntax
errorEntityNotFound,
errorInvalidInput
},
config, // Configuration
handler: {
adapterHandler, // Other handlers
otherHandler
}
}) =>
async function realmEntityAction(
params: Parameters<Handler>[0],
$meta: IMeta
): ReturnType<Handler> {
const processed = helperFunction(params.param1);
const result = await adapterHandler({
data: processed,
count: params.param2
}, $meta);
return {
result: result.value
};
}
);
Library Function
import {library} from '@feasibleone/blong';
export default library(
({errors: {errorInvalidInput}}) =>
function helperFunction(input: string): string {
if (!input) {
throw errorInvalidInput();
}
return input.toUpperCase();
},
);
API Parameter: Destructuring
The api parameter provides access to framework and realm functionality:
Available Properties
handler(
({
// Framework libraries
lib: {
error, // Error factory
type, // TypeBox (for manual validation)
bitsyntax, // Binary protocol parser
sum, // User-defined library function
rename, // Rename test arrays
},
// Domain errors (defined in error layer)
// Simplified syntax (recommended):
errors: {
errorEntityNotFound, // Maps to 'entity.notFound'
errorInvalidInput, // Maps to 'invalidInput'
errorPermissionDenied, // Maps to 'permission.denied',
},
// Legacy syntax (backwards compatible):
// errors: {
// 'entity.notFound': errorEntityNotFound,
// 'invalidInput': errorInvalidInput,
// 'permission.denied': errorPermissionDenied
// },
// Configuration for this component
config: {timeout, maxRetries, apiKey},
// Logger instance
log,
// Other handlers (from imports)
handler: {sqlUserFind, httpNotificationSend, otherRealmHandler},
}) => {
},
);
File Organization
One Handler Per File
Benefits:
- Fast discovery:
ctrl+p uua finds userUserAdd.ts
- Easier code review
- Clear boundaries
- Git-friendly diffs
Convention:
- File name = handler name
userUserAdd.ts exports userUserAdd handler
validateEmail.ts exports validateEmail library function
Folder Structure
orchestrator/
├── dispatch.ts
└── entity/
├── ~.schema.ts # Auto-validation
├── helperLib.ts # Library function
├── realmEntityAction1.ts # Handler
├── realmEntityAction2.ts # Handler
└── realmEntityAction3.ts # Handler
Group name: realmname.entity (referenced in imports)
Calling Other Handlers
From Orchestrator
export default handler(
({
handler: {
sqlUserFind, // Database adapter
paymentTransferCreate, // Other orchestrator
httpNotificationSend, // HTTP adapter
},
}) =>
async function userUserNotify(params, $meta) {
const user = await sqlUserFind({userId: params.userId}, $meta);
const payment = await paymentTransferCreate(
{
userId: params.userId,
amount: 100,
},
$meta,
);
await httpNotificationSend(
{
email: user.email,
subject: 'Payment Created',
body: `Payment ${payment.id} created`,
},
$meta,
);
return {success: true};
},
);
Using $meta
The $meta parameter carries context:
async function handlerName(params, $meta) {
await otherHandler(params, $meta);
await adapterHandler(params, {
...$meta,
method: 'specificOperationId',
});
await riskyHandler(params, {
...$meta,
expect: 'expectedErrorType',
});
}
Error Handling
Throwing Domain Errors
export default handler(
({errors}) =>
async function userUserFind(params, $meta) {
if (!params.userId) {
throw errors.invalidInput({
field: 'userId',
reason: 'required',
});
}
const user = await sqlUserFind({id: params.userId}, $meta);
if (!user) {
throw errors.userNotFound({userId: params.userId});
}
return user;
},
);
Wrapping External Errors
export default handler(
({errors}) =>
async function callExternalAPI(params, $meta) {
try {
return await externalApiCall(params, $meta);
} catch (error) {
if (error.code === 'TIMEOUT') {
throw errors.externalTimeout({cause: error});
}
throw errors.externalError({
message: error.message,
cause: error,
});
}
},
);
Configuration Access
Handlers can access configuration provided either through the framework's merged config or from a
co-located config.ts file in the handler folder.
export default handler(
({config}) =>
async function processWithTimeout(params, $meta) {
const timeout = config.timeout || 5000;
return Promise.race([
actualProcessing(params, $meta),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout)),
]);
},
);
Folder-Level Configuration (config.ts)
Place a config.ts file in any handler folder to define configuration for all handlers in that
folder. The file supports activation-based config (default, dev, prod, etc.) using the same
pattern as server.ts, keeping environment-specific values co-located with the handlers that use
them.
realmname/
└── orchestrator/
└── payment/
├── config.ts ← default config for payment handlers
├── paymentTransferSend.ts
└── paymentTransferReceive.ts
export default {
default: {
timeout: 30000,
retryCount: 3,
endpoint: 'https://api.payment.example.com',
},
dev: {
endpoint: 'https://api.dev.payment.example.com',
},
};
Handlers in the same folder receive this config automatically:
export default handler(
({config}: {config: {timeout: number; retryCount: number; endpoint: string}}) =>
async function paymentTransferSend(params, $meta) {
},
);
Overriding Folder Config via Realm
The realm's server.ts can override folder config values using the namespace config property.
Use this for values that cannot live in source code (e.g. production secrets, deployment-specific URLs):
import {realm} from '@feasibleone/blong';
export default realm(blong => ({
url: import.meta.url,
config: {
prod: {
namespace: {
payment: {
endpoint: 'https://api.prod.payment.example.com',
},
},
},
},
}));
Priority order: Realm namespace override > config.ts active environment activation > config.ts default
Automatic Validation
Define Handler Type
type Handler = ({
/** @description "Parameter description" */
param1: string;
param2?: number; // Optional parameter
}) => Promise<{
result: string;
}>;
Create ~.schema.ts
Place ~.schema.ts in handler folder:
- Auto-regenerates when handler types change
- Provides validation schemas
- Generates API documentation
Use Types in Handler
export default handler(
() =>
async function handlerName(
params: Parameters<Handler>[0],
$meta: IMeta,
): ReturnType<Handler> {
return {result: params.param1.toUpperCase()};
},
);
Best Practices
- Semantic Naming: Use subject-object-predicate consistently
- One Handler Per File: Easier navigation and review
- Type Definitions: Define Handler types for validation
- Library Functions: Extract reusable logic
- Error Handling: Use domain errors, not generic exceptions
- Async by Default: Framework converts all handlers to async
- $meta Propagation: Always pass $meta to handler calls
- Minimal Dependencies: Import only what you need from api
- Documentation: Use JSDoc descriptions in types
- Test Coverage: Write test handlers for all business handlers
- Co-locate Config: Use
config.ts in handler folders for folder-level defaults instead of putting config in server.ts
Handler Conversion
All handlers are converted to async functions:
export default handler(
() =>
function syncHandler(params) {
return {result: 'done'};
},
);
async function syncHandler(params) {
return {result: 'done'};
}
Examples from Codebase
- API handler:
core/test/demo/orchestrator/subject/subjectNumberSum.ts
- Library function:
core/test/demo/orchestrator/subject/sum.ts
- Adapter handler:
core/test/demo/adapter/http.ts
- TCP codec:
core/test/payshield/adapter/tcp/encode.ts
- Multiple handlers:
ml/payment/orchestrator/transfer/
- Folder config:
core/test/nscfg/orchestrator/cfg/config.ts