Skip to content

Deep Dive: Application

Extend BaseApplication. The three classes above it exist so a host that cannot open a socket - a browser Worker, a test harness - can still serve the same controllers.

Files:

  • packages/kernel/src/base/applications/abstract.ts
  • packages/kernel/src/base/applications/rest.ts
  • packages/core-server/src/base/applications/server.ts
  • packages/core-server/src/base/applications/base.ts
  • packages/kernel/src/base/applications/common/types/

Quick Reference

Each layer adds one capability. The first two ship from @venizia/ignis-kernel and touch no node builtin; the last two ship from @venizia/ignis.

ClassAddsKey Methods
AbstractApplicationconfig, lifecycle hooks, the DI containerinit(), registerPostStartHook(), registerPostStopHook()
RestApplicationthe two OpenAPIHono routersgetServer(), getRootRouter(), inspectRoutes()
ServerApplicationthe socketstart(), stop(), getServerHost(), getServerPort(), getServerAddress()
BaseApplicationresource registration, secrets, the server boot sequencecomponent(), controller(), service(), repository(), dataSource(), registerArtifacts(), validateEnvs()

NOTE

Every symbol still resolves from @venizia/ignis - packages/core-server re-exports the kernel wholesale. The split changed no import path.

AbstractApplication

Config, lifecycle and the container. No router, no server. Extends Container.

typescript
abstract class AbstractApplication extends Container

Constructor

typescript
constructor(opts: { scope: string; config: IApplicationConfigs })

The constructor:

  1. Merges the provided config with defaults, taking host and port from getEnvServerHost() / getEnvServerPort() and falling back to localhost:3000
  2. Resolves asyncContext.enable from getDefaultAsyncContextEnabled()
  3. Sets projectRoot from getProjectRoot()

Port 0 survives that merge on purpose - it asks the operating system for an ephemeral port.

The constructor binds nothing. registerCoreBindings() runs from init(), and init() is not called for you - see Lifecycle below.

The four constructor hooks

getEnvServerHost(), getEnvServerPort(), getDefaultAsyncContextEnabled() and getProjectRoot() return undefined, undefined, false and '' here. ServerApplication overrides all four to restore server behaviour - process.env.HOST, process.env.PORT, true, and process.cwd(). The kernel layers read no process, because a browser Worker has none.

WARNING

All four run inside this constructor, before any subclass field is assigned. An override must return a literal or read module-level state only. Reading this.something from one yields undefined, silently. getProjectRoot() is the one applications most often override - keep it free of instance state.

RestApplication

Adds the routers, and nothing that opens a socket.

typescript
abstract class RestApplication<
  AppEnv extends Env = Env,
  AppSchema extends Schema = {},
  BasePath extends string = '/',
> extends AbstractApplication

It builds the two OpenAPIHono instances - the main server and the rootRouter - and binds them as APPLICATION_SERVER and APPLICATION_ROOT_ROUTER.

ServerApplication

Adds start(), stop() and the runtime detection that picks Bun.serve or @hono/node-server.

typescript
abstract class ServerApplication<
  AppEnv extends Env = Env,
  AppSchema extends Schema = {},
  BasePath extends string = '/',
> extends RestApplication<AppEnv, AppSchema, BasePath>
  implements IApplication<AppEnv, AppSchema, BasePath>
  1. Auto-detects the runtime (Bun or Node.js)

Key Features

FeatureDescription
Hono InstanceCreates and holds two OpenAPIHono instances - a main server and a root router
Runtime DetectionAuto-detects Bun or Node.js via RuntimeModules.detect() and uses the appropriate server implementation
Core BindingsRegisters CoreBindings.APPLICATION_INSTANCE, CoreBindings.APPLICATION_SERVER, and CoreBindings.APPLICATION_ROOT_ROUTER
Lifecycle ManagementDefines abstract methods (preConfigure, postConfigure, setupMiddlewares, staticConfigure, initialize, getAppInfo)
Environment ValidationValidates all registered applicationEnvironment keys are non-empty (unless ALLOW_EMPTY_ENV_VALUE is set)
Post-Start HooksSupports registering hooks that execute after the server starts

Abstract Methods

These must be implemented by subclasses:

MethodSignaturePurpose
getAppInfo()() => ValueOrPromise<IApplicationInfo>Return application metadata (name, version, description)
preConfigure()() => ValueOrPromise<void>Register resources before framework auto-configuration
postConfigure()() => ValueOrPromise<void>Logic after all resources are configured
staticConfigure()() => voidPre-DI static setup (synchronous)
setupMiddlewares(opts?)(opts?: { middlewares?: Record<string | symbol, any> }) => ValueOrPromise<void>Register Hono middlewares
initialize()() => Promise<void>Full initialization sequence

Public Methods

MethodReturn TypeDescription
getProjectConfigs()IApplicationConfigsReturns the merged application config
getProjectRoot()stringReturns process.cwd() and binds it to CoreBindings.APPLICATION_PROJECT_ROOT
getRootRouter()OpenAPIHonoReturns the root router instance
getServerHost()stringReturns the configured host
getServerPort()numberReturns the configured port
getServerAddress()stringReturns host:port string
getServer()OpenAPIHonoReturns the main Hono server instance
getServerInstance()TBunServerInstance | TNodeServerInstance | undefinedReturns the underlying runtime server instance
registerPostStartHook(opts)voidRegister a hook to run after server start
init()voidCalls registerCoreBindings()
start()Promise<void>Runs initialize(), setupMiddlewares(), mounts root router, starts the server, then runs post-start hooks
stop()voidStops the server (calls .stop() for Bun, .close() for Node.js)

start() Method Flow

Server Types

typescript
// Bun server instance
type TBunServerInstance = ReturnType<typeof Bun.serve>;

// Node.js server instance (from @hono/node-server)
type TNodeServerInstance = any;

The server is stored as a discriminated union based on runtime:

typescript
protected server:
  | { hono: OpenAPIHono; runtime: 'bun'; instance?: TBunServerInstance }
  | { hono: OpenAPIHono; runtime: 'node'; instance?: TNodeServerInstance };

BaseApplication

Extends ServerApplication with concrete lifecycle implementations, resource registration and secrets hydration. Implements IRestApplication. This is the class your application extends.

typescript
abstract class BaseApplication
  extends ServerApplication
  implements IRestApplication

Resource Registration Methods

The six methods below bind classes to the DI container with conventional keys. Each of the five single-class methods reads the class's decorator defaults (binding, scope, allowOverride, set through @controller, @service, ...); an explicit opts at the call site wins over them. registerArtifacts is what the registerArtifacts boot step calls with configs.artifacts - see Artifact Registration.

MethodDI Binding Key ConventionScope
component(ctor, opts?)components.{Name}Singleton
controller(ctor, opts?)controllers.{Name}Singleton
service(ctor, opts?)services.{Name}Transient
repository(ctor, opts?)repositories.{Name}Transient
dataSource(ctor, opts?)datasources.{Name}Singleton
registerArtifacts(index)one call per listed class, in dependency orderper class

TIP

All registration methods accept an optional opts.binding parameter to override the default namespace-based key:

typescript
this.controller(UserController, {
  binding: { namespace: 'controllers', key: 'CustomUserController' },
});

Method Signatures

typescript
component<Base extends BaseComponent>(ctor: TClass<Base>, opts?: TMixinOpts): Binding<Base>
controller<Base>(ctor: TClass<Base>, opts?: TMixinOpts): Binding<Base>
service<Base extends IService>(ctor: TClass<Base>, opts?: TMixinOpts): Binding<Base>
repository<Base extends IRepository>(ctor: TClass<Base>, opts?: TMixinOpts): Binding<Base>
dataSource<Base extends IDataSource>(ctor: TClass<Base>, opts?: TMixinOpts): Binding<Base>
registerArtifacts(index: TArtifactIndexInput): Promise<void>
protected registerConfiguredArtifacts(): Promise<void>

Where TMixinOpts is:

typescript
type TMixinOpts = {
  binding?: { namespace: string; key: string };
  allowOverride?: boolean;
};

The options describe the registration, never the artifact: what a class needs goes on the class.

MethodBinding scopeWhy
component, dataSource, controllerSINGLETONOne instance per application - a controller is mounted once, a datasource holds one pool
service, repositoryTRANSIENTA new instance per resolution, so each injection point owns its own

binding is optional - omit it and the method derives { namespace, key } from the class name. allowOverride defaults to true, matching bind()'s own silent-overwrite behavior: register the same key twice and the second registration wins, no warning. Set it to false to make a same-key re-registration throw instead of silently shadowing the first one. bootChecks.binding.allowOverride: false flips that default for every registration in the application; allowOverride: true on a registration opts it out again.

typescript
this.controller(UserController, { allowOverride: false });

Static File Serving

typescript
static(opts: { restPath?: string; folderPath: string }): this

Serves static files using the appropriate runtime handler (hono/bun for Bun, @hono/node-server/serve-static for Node.js). The restPath defaults to '*'.

typescript
this.static({ restPath: '/public/*', folderPath: './public' });

Artifact registration

typescript
async registerArtifacts(index: TArtifactIndexInput): Promise<void>
protected async registerConfiguredArtifacts(): Promise<void>

registerArtifacts registers one index or a composition of indexes: datasources, then components (plus their @provide keys), repositories, services, controllers; a class's when may skip it and order sorts within a kind. registerConfiguredArtifacts is the boot step that passes configs.artifacts to it. Full behavior: Artifact Registration.

registerDynamicBindings

Protected. Scans one binding namespace, resolves each binding and calls its configure(), then re-scans until a pass adds nothing new. An artifact that a configure() registers is therefore picked up in the same call.

typescript
protected async registerDynamicBindings<T extends IConfigurable>(opts: {
  namespace: TBindingNamespace;
  onBeforeConfigure?: (opts: { binding: Binding<T> }) => Promise<void>;
  onAfterConfigure?: (opts: { binding: Binding<T>; instance: T }) => Promise<void>;
}): Promise<void>
ParameterTypeDescription
namespaceTBindingNamespaceBinding namespace to scan (e.g., 'components', 'datasources')
onBeforeConfigurecallbackRuns before each binding's configure()
onAfterConfigurecallbackRuns after configure(), once the binding is already marked configured

Configured keys are remembered per namespace, so a second call over the same namespace touches only what the first one missed.

initialize() Method Flow

Startup sequence executed by the initialize() method:

Fifteen steps. getBootSequence() returns them as data (ServerBootSteps names each one), runBootSequence() logs Boot step n/15 <name> per step, and a subclass inserts its own step with BootSequence.insertAfter({ steps, target, step }).

HookWhen to UseNotes
staticConfigure()Pre-DI static setup (static files, etc.)Synchronous, called before registerArtifacts
registerArtifactsFramework registers every class in configs.artifactsDecorator when conditions run here, before preConfigure
preConfigure()Register what the index cannot express - registry calls, hand-made bindingsNothing instantiated yet - order doesn't matter
register...()Framework iterates bindings and instantiates classesDataSources initialized first (other layers depend on them)
postConfigure()Logic after all resources configuredDo not register new datasources/components/controllers here - they won't auto-configure

registerDefaultMiddlewares

Automatically registers these default middlewares during initialize():

  1. Error handler (AppErrorMiddleware) - with optional rootKey from configs.error.rootKey
  2. Async context storage (contextStorage) - enabled by default via configs.asyncContext.enable
  3. Not-found handler (notFoundHandler)
  4. RequestTrackerComponent - assigns x-request-id to every request, includes request body parsing
  5. Emoji favicon - defaults to the flame emoji, configurable via configs.favicon

registerControllers and Transport Support

The registerControllers() method supports multiple transport protocols via the transports config:

typescript
// In your application config
{
  transports: ['rest'],        // Default: REST only
  transports: ['rest', 'grpc'], // Enable both REST and gRPC
  transports: ['grpc'],        // gRPC only
}

For each transport in the array, the corresponding component (RestComponent or GrpcComponent) is instantiated and configured. If gRPC controllers are discovered but the 'grpc' transport is not enabled, a warning is logged.

registerComponents

A component may register more components while it is configured, at any nesting depth, and may add a datasource of its own. Contributed datasources are configured by registerContributedDataSources(), one flat sweep that runs after every component has finished - not after each component in turn. A component that uses a datasource an earlier component contributed sees it unconfigured until that sweep runs.

IApplicationConfigs

typescript
interface IApplicationConfigs {
  host?: string;                          // Server host (default: process.env.HOST || 'localhost')
  port?: number;                          // Server port (default: process.env.PORT || 3000)
  server?: { idleTimeout?: number; maxRequestBodySize?: number }; // Bun.serve options, Bun runtime only
  projectRoot?: string;                   // Server only: bound as APPLICATION_PROJECT_ROOT (default: process.cwd())
  path: { base: string; isStrict: boolean }; // Base path config (required)
  requestId?: { isStrict: boolean };      // Request ID validation
  favicon?: string;                       // Favicon emoji (default: '🔥')
  error?: { rootKey: string };            // Error response root key
  asyncContext?: { enable: boolean };     // Hono async context storage (default: true)
  artifacts?: TArtifactIndexInput;        // Generated indexes registered before preConfigure
  debug?: { shouldShowRoutes?: boolean }; // Show registered routes on startup
  transports?: TControllerTransport[];    // Controller transports: 'rest' | 'grpc' (default: ['rest'])
  [key: string]: any;                     // Extensible (e.g. strictPath?: boolean - Hono strict path matching, default: true)
}

TArtifactIndexInput

typescript
interface IArtifactIndex {
  dataSources?: ReadonlyArray<TClass<IDataSource>>;
  components?: ReadonlyArray<TClass<BaseComponent>>;
  repositories?: ReadonlyArray<TClass<IRepository>>;
  services?: ReadonlyArray<TClass<IService>>;
  controllers?: ReadonlyArray<TClass<unknown>>;
}

type TArtifactIndexInput = IArtifactIndex | TArtifactIndexInput[];

bootOptions is removed - see the changelog for migration.

TControllerTransport

typescript
class ControllerTransports {
  static readonly REST = 'rest';
  static readonly GRPC = 'grpc';
}

type TControllerTransport = 'rest' | 'grpc';

IApplicationInfo

typescript
interface IApplicationInfo {
  name: string;
  version: string;
  description: string;
  author?: { name: string; email: string; url?: string };
  [extra: string | symbol]: any;
}

CoreBindings

Core binding keys used for fundamental application components:

KeyValueDescription
APPLICATION_INSTANCE'@app/instance'The application instance itself
APPLICATION_SERVER'@app/server'The server object (hono + runtime + instance)
APPLICATION_CONFIG'@app/config'Application configuration
APPLICATION_PROJECT_ROOT'@app/project_root'Project root directory (process.cwd())
APPLICATION_ROOT_ROUTER'@app/router/root'The root OpenAPIHono router
APPLICATION_ENVIRONMENTS'@app/environments'Application environment variables
APPLICATION_MIDDLEWARE_OPTIONS'@app/middleware_options'Middleware configuration options

BindingNamespaces

Standard namespaces for organizing DI bindings:

NamespaceValueUsed By
COMPONENT'components'component()
DATASOURCE'datasources'dataSource()
REPOSITORY'repositories'repository()
MODEL'models'Model bindings
SERVICE'services'service()
MIDDLEWARE'middlewares'Middleware bindings
PROVIDER'providers'Provider bindings
CONTROLLER'controllers'controller()

Mixin Interfaces

BaseApplication implements several mixin interfaces that define its capabilities:

InterfaceMethodsDescription
IComponentMixincomponent(), registerComponents()Component registration and lifecycle
IControllerMixincontroller(), registerControllers()Controller registration and route mounting
IRepositoryMixindataSource(), repository()DataSource and repository registration
IServiceMixinservice()Service registration
IStaticServeMixinstatic()Static file serving

NOTE

There is also an IServerConfigMixin interface defined in the mixins types that declares staticConfigure(), preConfigure(), postConfigure(), and getApplicationVersion(), though BaseApplication inherits the first three from AbstractApplication.

Middleware Configuration Types

These types are used when configuring middlewares via setupMiddlewares():

typescript
interface IMiddlewareConfigs {
  requestId?: IRequestIdOptions;
  compress?: ICompressOptions;
  cors?: ICORSOptions;
  csrf?: ICSRFOptions;
  bodyLimit?: IBodyLimitOptions;
  ipRestriction?: IBaseMiddlewareOptions & IIPRestrictionRules;
  [extra: string | symbol]: any;
}

interface IBaseMiddlewareOptions {
  enable: boolean;
  path?: string;
  [extra: string | symbol]: any;
}

See Also