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 protocolsorchestrator- Business process coordination between adapterserror- Domain-specific error definitionstest- Test automation (dev/build only)eft- Electronic Funds Transfer (high TPS OLTP requirements)
Browser-Side Layers (auto-detected)
backend- Browser adapter talking to servercomponent- React UI componentsbrowser- 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 activation. This eliminates the need for an explicit children array or activation config in the parent server.ts.
// myCustomLayer/layer.server.ts
import {layer} from '@feasibleone/blong';
export default layer({
default: true, // active in all environments
microservice: true, // additionally active in microservice deployment
});
// myBrowserLayer/layer.browser.ts
import {layer} from '@feasibleone/blong';
export default layer({
integration: true, // only active during integration testing
});
Well-Known Layer Default Activations
| Folder | Server default | Browser default |
|---|---|---|
error |
{default: true} |
— |
sim |
{integration: true} |
- |
adapter |
{default: true} |
- |
orchestrator |
{default: true} |
— |
gateway |
{default: true} |
— |
browser |
{default: true} |
— |
backend |
{default: true} |
|
component |
{default: true} |
|
test |
{integration: true} |
Self-Contained Layer Pattern
Adapter Layer Definition
// adapter/db.ts - self-contained with config and validation
import {adapter} from '@feasibleone/blong';
export default adapter(blong => ({
extends: 'adapter.knex',
// Layer's own validation schema
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()),
}),
// Layer's own configuration per environment
activation: {
default: {
namespace: 'db/$subject',
imports: '$subject.db',
},
dev: {
logLevel: 'trace',
},
prod: {
logLevel: 'warn',
},
},
}));
Orchestrator Layer Definition
// orchestrator/dispatch.ts - self-contained with config and validation
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.
// server.ts — only when realm-level config is needed
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:
// adapter/db.ts - imports declared inline
export default adapter(blong => ({
extends: 'adapter.knex',
activation: {
default: {
namespace: ['user', 'role'],
imports: ['user.user', 'user.role'], // handler groups loaded
},
},
}));
Implementation Patterns
Error Layer
// realmname/error/error.ts
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: {
// Activate for automated testing
test: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
test: true,
},
// Activate for microservice deployment
microservice: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
},
// Activate for single realm dev focus
realm: {
adapter: true,
orchestrator: true,
},
// Development with full stack
dev: {
error: true,
adapter: true,
orchestrator: true,
gateway: true,
},
},
}));
One Handler Per File Pattern
Benefits
- Fast Discovery: Use
ctrl+pin VS Code to find handlers quickly- Example:
ctrl+p uuafindsuserUserAdd.ts
- Example:
- 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
// 1. Create adapter/newadapter.ts
export default adapter(() => ({
extends: 'adapter.http',
}));
// 2. Update server.ts validation + config + children
// (3 edits in server.ts)
After (New Pattern) - Touch 1 file
// 1. Create adapter/newadapter.ts - everything co-located
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,
},
},
}));
// 2. Done! Framework uses the layer's own config.
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.tsfor 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/