Service Action Engine
Type: Reference / Specification
Path: engine/
1. Purpose
The Service Action Engine provides a high-performance, $O(1)$ routing and introspection layer for business operations, and a unified execution pipeline. It maps a flat array of domain-specific Services (and their nested Actions) into pre-computed memory structures upon initialization.
This engine is designed to sit below the HTTP/RPC transport layer (e.g., Hono), decoupling the knowledge of available actions from the transport mechanism used to invoke them.
1.1 Responsibilities
- Initialization: Parse the
servicesarray exactly once on boot. - Introspection: Provide lightweight, zero-latency metadata for available services and actions to enable dynamic discovery.
- Routing: Provide strict $O(1)$ memory pointer lookups for specific actions based on
serviceNameandactionName. - Execution Pipeline: Process the full action lifecycle safely, including Global/Action-level Hooks, Payload Validation (Zod), and safe Handler execution.
- Diagnostics: Emit timing and status information via
createDiagnosticsLogfromutils/diagnostics-log.tswhendiagnosticsis enabled. Seedocs/internals/logging.mdsection 7. - Result Pattern Enforcement: Ensure all internal engine methods return a
Result<T, E>from theslang-tslibrary to eliminatetry/catchrequirements in the transport layer.
1.2 Non-Goals
- HTTP Routing: The engine has no concept of HTTP methods, headers, or paths.
2. Architecture and Data Structures
To achieve guaranteed $O(1)$ lookups and prevent latency spikes, the engine pre-computes three internal data structures during createEngine:
serviceSummaries: An array ofServiceSummaryobjects used for fast enumeration of all available services.serviceActionsStore: A dictionary mapping aserviceNameto an array of lightweightActionSummaryobjects. This avoids sending bulky schema/handler definitions during introspection.actionStore: A nested dictionary (Record<serviceName, Record<actionName, Action>>) that holds the exact memory pointers to the fullActionobjects for execution routing.
The engine execution pipeline helpers are extracted into pipeline.ts to keep the code modular and under the 400 LOC limit.
3. Public API
The engine exposes four strictly-typed methods. All methods return a slang-ts Result.
3.1 getServices()
Returns an array of all registered services.
Returns: Result<ServiceSummary[], string>
3.2 getServiceActions(serviceName: string)
Returns lightweight metadata for all actions within a specific service.
Returns: Result<ActionSummary[], string>
Note: The validation property is a boolean indicating whether the action has a defined Zod schema (!!action.validation).
3.3 getAction(serviceName: string, actionName: string)
Returns the full, executable Action object. Typically used internally by the execution pipeline.
Returns: Result<Action, string>
3.4 executeAction(serviceName: string, actionName: string, payload: unknown, nileContext: NileContext)
Executes an action through the full pipeline (Global Before Hook -> Action Before Hooks -> Validation -> Handler -> Action After Hooks -> Global After Hook). The caller must provide a NileContext instance — the engine never creates one internally.
Returns: Promise<Result<unknown, string>>
4. Execution Pipeline
When executeAction is called, the following steps run in sequence:
- Global Before Hook (
onBeforeActionHandler) — Pass/fail guard only, does not mutate payload - Action-Level Before Hooks (
action.hooks.before) — Sequential, output becomes next input (mutates payload) - Zod Validation — Uses
action.validation.safeParse()withprettifyErrorfor formatting - Main Handler — Core business logic
- Action-Level After Hooks (
action.hooks.after) — Sequential, mutates result - Global After Hook (
onAfterActionHandler) — Final cleanup/logging
4.1 Hook Failure Behavior
- Each
HookDefinitionhas anisCritical: booleanflag isCritical: true— if the hook returnsError throws, the pipeline halts immediatelyisCritical: false— failure is logged but execution continues with the previous value
4.2 Pipeline Response Mode
If an action sets result: { pipeline: true }, the return includes the full hook execution log:
5. Crash Safety (safeTry)
All handler and hook invocations in pipeline.ts are wrapped in safeTry from slang-ts. This prevents uncaught exceptions from crashing the process.
Protected call sites:
runHook— action-level before/after hook handlersrunHandler— main action handlerrunGlobalBeforeHook— global before hookrunGlobalAfterHook— global after hook
If a handler throws instead of returning a Result, safeTry catches the exception and returns Err(error.message). The pipeline then handles it identically to a handler-returned Err.
6. Constraints and Failure Modes
6.1 Constraints
- Memory Bound: Because the engine loads all
servicesand their dependencies (Zod schemas, DB models via imports) into memory upfront, it is designed for persistent, long-running server environments (e.g., standard Node.js/Bun containers), not aggressive cold-start environments. - Immutability: The initialized stores (
actionStore, etc.) are closed over in the factory function and cannot be modified at runtime. Dynamic injection of actions post-boot is not supported. - File Size: The core
engine.tsmust remain under 400 LOC, relying onpipeline.tsfor pipeline steps.
6.2 Failure Modes
- Missing Service/Action: Calling
getServiceActions,getAction, orexecuteActionwith an unregistered name will immediately return anErr(string)result. The transport layer must handle this by returning a404 Not Foundor equivalent error to the client. - Duplicate Detection: The engine throws immediately on boot if it encounters duplicate service names or duplicate action names within the same service. This prevents silent overwrites and catches configuration errors early.
- Duplicate service name →
Error: Duplicate service name '<name>'. Service names must be unique. - Duplicate action name →
Error: Duplicate action name '<name>' in service '<service>'. Action names must be unique within a service. - The same action name in different services is valid and does not throw.
- Duplicate service name →
7. Key Types
All types below are exported from index.ts and defined in engine/types.ts.
7.1 EngineOptions
Configuration passed to createEngine:
createEngine is consumed internally by createNileServer — developers configure these values via ServerConfig.
7.2 HookContext
Tracks the full lifecycle state of a single action execution. Attached to NileContext.hookContext and reset at the start of each executeAction call.
state— mutable key-value store for hooks to share data within a single executionlog— accumulatedHookLogEntryrecords from before/after hook phases
7.3 HookLogEntry
A single hook execution record:
7.4 HookDefinition
Declares a hook as a reference to another action in the system:
See section 4.1 for isCritical behavior.
7.5 ActionResultConfig
Controls the shape of executeAction return values:
When pipeline: true, the result includes the full hook execution log alongside the data. See section 4.2.
8. Factory Functions
The @nilejs/nile package exports typed identity functions for defining services and actions with full type inference.
8.1 createAction
Creates a single action with full type inference. No runtime overhead — returns the config as-is.
8.2 createActions
Creates multiple actions at once. This is optional — you can also pass action arrays directly.
8.3 createService
Creates a service with full type inference.
8.4 createServices
Creates multiple services at once.
8.5 Recommended Project Structure
For larger applications, organize actions one-per-file in domain folders. Define all services in a single services.config.ts file that imports the actions and exports the services array. No barrel (index.ts) file per service folder is needed.
Keep database code in a separate db/ directory — schema definitions, client setup, and model files that encapsulate all data access logic. See Database Utilities for the full model file pattern.
Action handlers call model functions for data access — they should not contain raw database queries. Models handle validation, error logging, and return Result types that handlers forward to the client.
Each action file defines the handler inline (not exported) and only exports the action:
The services.config.ts file imports all actions and defines services using createServices:
For larger applications, you may extract the server configuration into a separate server.config.ts that imports the services array. For smaller projects, defining the config directly in index.ts is equally valid.
8.6 Alternative — Barrel File Pattern
An alternative (not recommended for most projects) is to create a barrel file per service folder using createService. This adds a file per domain but can be useful for very large codebases where you want explicit service boundaries: