| name | blong-layer |
| description | Organize handlers into named functional groups within a Blong realm. Layers include gateway (API), adapter (external systems), orchestrator (business logic), error (domain errors), and test (automation). Make sure to use this skill whenever creating a new layer in a realm, setting up the folder structure for handlers, or configuring which layers activate in which environment — even if the user just says 'organize this' or 'add a new layer'. |
Implementing a Layer
Overview
Layers are named groups of handlers that organize code by functional concern within a realm. They enable clear separation of responsibilities and support the framework's modular architecture.
Key Pattern: Layers are self-contained — each layer file defines its own configuration and validation, co-located with its implementation. No need to maintain configuration in a central server.ts.
Purpose
- Separation of Concerns: Group related functionality together
- Co-located Config: Each layer owns its configuration and validation
- Code Organization: Clear folder structure for handlers
- Deployment Control: Layers can be activated/deactivated per environment
- Team Coordination: Different teams can work on different layers
Recommended Layer Names
Server-Side Layers (auto-detected)
gateway - API gateway: routes, validation, documentation (minimal business logic)
adapter - External system communication: SQL, HTTP, FTP, mail protocols
orchestrator - Business process coordination between adapters
error - Domain-specific error definitions
test - Test automation (dev/build only)
eft - Electronic Funds Transfer (high TPS OLTP requirements)
Browser-Side Layers (auto-detected)
backend - Browser adapter talking to server
component - React UI components
browser - Server-side code serving browser assets
Folder Structure
Typical Realm with Layers
Well-known layer folders are auto-discovered — no layer.server.ts needed:
realmname/
├── server.ts # Optional — only for realm-level config/validation
├── error/ # Auto-activated (well-known name)
│ └── error.ts
├── adapter/ # Auto-activated (well-known name)
│ ├── db.ts # Self-contained database adapter (with config)
│ ├── http.ts # Self-contained HTTP adapter (with config)
│ └── db/ # Handler group: realmname.db
│ ├── userUserAdd.ts
│ └── userUserFind.ts
├── orchestrator/ # Auto-activated (well-known name)
│ └── dispatch.ts # Self-contained orchestrator (with config)
└── gateway/ # Auto-activated (well-known name)
└── api/
└── user.yaml
Custom (non-well-known) layer folders need a layer.server.ts or layer.browser.ts:
realmname/
└── myCustomLayer/
└── layer.server.ts # Required: declares activation for non-well-known folder
layer.server.ts / layer.browser.ts
For folders with non-standard names, add a layer.server.ts or layer.browser.ts to declare
which CLI intents activate the layer. This eliminates the need for an explicit children array
or activation config in the parent server.ts.
import {layer} from '@feasibleone/blong';
export default layer({
default: true,
microservice: true,
});
import {layer} from '@feasibleone/blong';
export default layer({
integration: true,
});
Well-Known Layer Default Intents
The key is the intent that activates the layer. {default: true} means the layer is always
active. Provide a layer.server.ts to override any entry.
| Folder | Server intent | Browser intent |
|---|
error | default (always) | — |
sim | integration | — |
adapter | default (always) | default (always) |
orchestrator | default (always) | — |
gateway | default (always) | — |
browser | default (always) | — |
backend | — | default (always) |
component | — | default (always) |
test | integration | integration |
See the blong-intent skill for the full intents reference and how to create custom intents.
Self-Contained Layer Pattern
Adapter Layer Definition
import {adapter} from '@feasibleone/blong';
export default adapter(blong => ({
extends: 'adapter.knex',
validation: blong.type.Object({
namespace: blong.type.Union([blong.type.String(), blong.type.Array(blong.type.String())]),
imports: blong.type.Union([blong.type.String(), blong.type.Array(blong.type.String())]),
logLevel: blong.type.Optional(blong.type.String()),
}),
activation: {
default: {
namespace: 'db/$subject',
imports: '$subject.db',
},
dev: {
logLevel: 'trace',
},
prod: {
logLevel: 'warn',
},
},
}));
Orchestrator Layer Definition
import {orchestrator} from '@feasibleone/blong';
export default orchestrator(blong => ({
extends: 'orchestrator.dispatch',
validation: blong.type.Object({
namespace: blong.type.Union([blong.type.String(), blong.type.Array(blong.type.String())]),
imports: blong.type.Union([
blong.type.String(),
blong.type.Array(blong.type.String()),
blong.type.Array(blong.type.RegExp()),
]),
validations: blong.type.Optional(
blong.type.Array(blong.type.Union([blong.type.String(), blong.type.RegExp()])),
),
destination: blong.type.Optional(blong.type.String()),
}),
activation: {
default: {
destination: 'db',
namespace: ['$subject'],
imports: [/^$subject\./],
validations: [/^$subject\.\w+\.validation$/],
},
},
}));
Realm Entry Point (Only When Needed)
server.ts is optional. It is only needed when there is realm-level config or validation shared across layers.
import {realm} from '@feasibleone/blong';
export default realm(blong => ({
url: import.meta.url,
validation: blong.type.Object({
myService: blong.type.Object({url: blong.type.String()}),
}),
activation: {
default: {
myService: {url: 'http://localhost:8080'},
},
},
}));
Handler Group Pattern
Group Naming Convention
Groups are named in format: realmname.foldername
Example:
- Realm:
user
- Folder:
orchestrator/user/
- Group name:
user.user
Referencing Groups
Groups are referenced in the imports property of the adapter/orchestrator:
export default adapter(blong => ({
extends: 'adapter.knex',
activation: {
default: {
namespace: ['user', 'role'],
imports: ['user.user', 'user.role'],
},
},
}));
Folder-Level Default Configuration (config.ts)
Each handler group folder can contain a config.ts file that defines configuration for all
handlers in that folder. The file supports activation-based config (default, dev, prod, etc.)
using the same pattern as server.ts, making the group self-contained with environment-specific
values co-located with the handlers that use them:
orchestrator/
├── dispatch.ts
└── payment/ # Handler group
├── config.ts ← default config for this group
├── ~.schema.ts
└── paymentTransferSend.ts
export default {
default: {
timeout: 30000,
retryCount: 3,
endpoint: 'https://api.payment.example.com',
},
dev: {
endpoint: 'https://api.dev.payment.example.com',
},
};
The realm's server.ts can override specific values using the namespace property nested in the
realm config. Use this for deployment-specific values (e.g. secrets or production URLs) that cannot
live in source code:
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: Realm namespace override > config.ts active environment activation > config.ts default
Implementation Patterns
Error Layer
export default {
userNotFound: 'User not found',
userExists: 'User already exists',
invalidEmail: 'Invalid email format',
permissionDenied: 'Permission denied',
};
Adapter Layer Structure
adapter/
├── db.ts # Self-contained database adapter
├── http.ts # Self-contained HTTP adapter
├── db/ # Handler group for db operations
│ ├── userAdd.ts
│ └── userFind.ts
└── http/ # Handler group for HTTP operations
├── send.ts
└── receive.ts
Orchestrator Layer Structure
orchestrator/
├── dispatch.ts # Self-contained orchestrator definition
├── entity1/ # Handler group: realmname.entity1
│ ├── ~.schema.ts # Auto-generated validation
│ ├── helper.ts # Library function
│ └── realmEntity1Action.ts
└── entity2/ # Handler group: realmname.entity2
├── ~.schema.ts
└── realmEntity2Action.ts
Gateway Layer Structure
gateway/
└── api/
├── entity1.yaml # OpenAPI spec for entity1
└── entity2.yaml # OpenAPI spec for entity2
Or with custom validation:
gateway/
└── entity/
├── entityAction1.ts # Manual validation
└── entityAction2.ts
Test Layer Structure
test/
└── test/ # Handler group: test.test
├── testEntity1.ts
├── testEntity2.ts
└── testWorkflow.ts
Layer Activation
In Realm Configuration (server.ts)
export default realm(blong => ({
config: {
test: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
test: true,
},
microservice: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
},
realm: {
adapter: true,
orchestrator: true,
},
dev: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
},
},
}));
One Handler Per File Pattern
Benefits
- Fast Discovery: Use
ctrl+p in VS Code to find handlers quickly
- Example:
ctrl+p uua finds userUserAdd.ts
- Easier Code Review: Smaller files, less nesting
- Better Isolation: Clear boundaries between handlers
- Git-Friendly: Smaller diffs, fewer conflicts
Naming Convention
File name = handler name:
- Handler:
userUserAdd → File: userUserAdd.ts
- Handler:
mathNumberSum → File: mathNumberSum.ts
- Library:
validateEmail → File: validateEmail.ts
Adding a New Adapter
Before (Old Pattern) - Touch 2 files
export default adapter(() => ({
extends: 'adapter.http',
}));
After (New Pattern) - Touch 1 file
export default adapter(blong => ({
extends: 'adapter.http',
validation: blong.type.Object({
url: blong.type.String(),
timeout: blong.type.Number(),
}),
activation: {
default: {
url: 'http://api.example.com',
timeout: 5000,
},
},
}));
Multi-Layer Example
Complete Realm Structure
payment/
├── server.ts # Minimal - activation only
├── error/
│ └── error.ts
├── adapter/
│ ├── db.ts # Self-contained: config + validation inside
│ ├── fspiop.ts # Self-contained: config + validation inside
│ └── db/
│ ├── paymentCreate.ts
│ └── paymentFind.ts
├── orchestrator/
│ ├── dispatch.ts # Self-contained: config + validation inside
│ ├── transfer/
│ │ ├── ~.schema.ts
│ │ ├── calculateFees.ts
│ │ ├── paymentTransferPrepare.ts
│ │ └── paymentTransferCommit.ts
│ └── quote/
│ ├── ~.schema.ts
│ └── paymentQuoteCreate.ts
├── gateway/
│ └── api/
│ ├── transfer.yaml
│ └── quote.yaml
└── test/
└── test/
├── testTransfer.ts
└── testQuote.ts
Best Practices
- Co-locate Config: Put layer configuration in the layer file, not in server.ts
- Clear Separation: Keep business logic in orchestrator, not gateway
- Consistent Naming: Use lowercase single words for layer names
- Group by Entity: Organize handlers by business entity within layers
- Library Functions: Extract reusable code into library functions
- Error Definitions: Always define errors in error layer first
- Test Coverage: Create test layer with comprehensive test handlers
- One File Per Handler: Follow the one handler per file pattern
- Validation: Use
~.schema.ts for automatic validation generation
- Import Order: Load layers in order: error → adapter → orchestrator → gateway → test
Examples from Codebase
- Complete realm:
core/test/demo/
- Payment realm:
ml/payment/
- Agreement realm:
ml/agreement/