REST Interface
Type: Reference / Specification
Path: rest/
1. Purpose
The REST interface exposes the Action Engine over HTTP via Hono. It implements a single-POST-endpoint architecture where all service communication flows through one route, discriminated by an intent field in the request body.
1.1 Responsibilities
- Request validation: Validates incoming JSON against a Zod schema before processing
- Intent routing: Dispatches
explore,execute, andschemaintents to dedicated handlers - Response mapping: Converts internal
Result<T, E>types to theExternalResponseshape at the HTTP boundary - Middleware application: CORS, rate limiting, and static file serving
- Diagnostics: Emit request routing information via
createDiagnosticsLogfromutils/diagnostics-log.tswhendiagnosticsis enabled. Seedocs/internals/logging.mdsection 7.
1.2 Non-Goals
- Business logic: The REST layer does not contain domain logic. It delegates to the engine.
2. Architecture
The REST module is split into three files to stay under the 400 LOC limit:
3. Endpoints
3.1 POST {baseUrl}/services
The single endpoint for all service interactions. The request body must conform to ExternalRequest:
The body is validated against a Zod schema. Invalid JSON or missing fields return 400.
3.2 GET /status
Health check endpoint. Only registered when config.enableStatus is true.
Returns:
3.3 404 Handler
All unmatched routes return:
3.4 Error Handler
All unhandled errors are caught by a global app.onError handler:
HTTPException: Returns the exception's status code and message in the standardExternalResponseshape. This allows middleware (rate limiter, user middleware) to throw intentional HTTP errors that pass through cleanly.- Unknown errors: Returns a generic
500 Internal Server Error. The real error message and stack trace are logged via diagnostics but never exposed to the client.
3.5 Discovery Protection
The explore and schema intents can be gated via RestConfig.discovery:
When discovery.enabled is false (the default), explore and schema requests return 403:
When discovery.secret is set, the request's payload.discoverySecret must match. Mismatches return 403:
Visibility Filtering
Actions can declare a visibility field:
If visibility.rest === false, the action is hidden from explore and schema responses. It can still be executed. Visibility only controls discoverability, not access.
4. Intent Handlers
Intent dispatch uses an object lookup (intentHandlers) rather than switch/if-else.
4.1 Explore
Discovers services and actions.
4.2 Execute
Runs an action through the engine pipeline. Wildcards are rejected. Both service and action must be specific.
Calls engine.executeAction(service, action, payload, nileContext) and maps the result.
4.3 Schema
Exports Zod validation schemas as JSON Schema (via z.toJSONSchema() from Zod v4).
Actions without a validation schema return null. Schema conversion failures are caught by safeTrySync and also return null.
5. Response Format
All responses use the ExternalResponse shape:
The toExternalResponse function handles the Result to ExternalResponse mapping:
Ok(value): if value is a plain object, it becomesdatadirectly. Arrays and primitives are wrapped as{ result: value }.Err(message):status: false, message is the error string,datais empty.
HTTP status codes: 200 for success, 400 for failures and validation errors, 404 for unmatched routes, 500 for unhandled errors (via app.onError). HTTPException errors pass through with their own status code.
6. Middleware
6.1 CORS
Applied first via applyCorsConfig from cors/cors.ts. See docs/internals/cors.md.
6.2 Rate Limiting
File: rest/middleware.ts, applyRateLimiting
Only applied when config.rateLimiting.limitingHeader is set. Uses hono-rate-limiter.
- Client key is extracted from the configured request header
- If the header is missing, falls back to IP-based identification:
x-forwarded-for→x-real-ip→"unknown-client" - The fallback is logged via diagnostics so operators can see when the configured header is absent
- Defaults: 100 requests per 15-minute window
6.3 Static File Serving
File: rest/middleware.ts, applyStaticServing
Only applied when config.enableStatic is true. Supports both "bun" and "node" runtimes.
- Serves files from the configured
staticDir(default:"./assets") at/assets/* - Auto-creates the directory if it doesn't exist (
mkdirSyncwithrecursive: true) - Dynamically imports the runtime-specific adapter (
hono/bunfor Bun,@hono/node-server/serve-staticfor Node) - The import result is cached after first successful load
- Import failures are caught by
safeTry. Static serving is silently skipped
7. Key Types
7.1 RestConfig
7.2 RateLimitConfig
7.3 ExternalRequest
7.4 ExternalResponse
7.5 DiscoveryConfig
7.6 UploadsConfig
7-step sequential validation chain: filename length → zero-byte → min size → file count → max file size → total size → MIME + extension allowlist. Fails fast on first error.
7.7 detectMimeType
Reads the first 12 bytes of a Blob or File to detect the actual MIME type from magic byte signatures. Returns null if unrecognized. Supported types:
This is a standalone utility. It is NOT auto-wired into the validation chain. Call it explicitly in your action handlers when you need to verify a file's actual type beyond the declared file.type.
8. Constraints
- Single POST endpoint: All service interactions go through
POST {baseUrl}/services. No per-action routes. - No streaming: Responses are JSON only. No SSE or chunked transfer.
- Rate limiter requires header config: Without
rateLimiting.limitingHeaderin config, rate limiting is not applied at all. When configured but the header is absent from a request, the limiter falls back to IP-based identification.
9. Failure Modes
- Invalid JSON body: Returns
400with "Invalid or missing JSON body" - Schema validation failure: Returns
400with Zod issue details indata.errors - Wildcard in execute: Returns
400with descriptive message - Missing service/action: Engine returns
Err, mapped to400viatoExternalResponse - Handler crash: Caught by
safeTryin the engine pipeline, returned asErr