App Builder
Introduction
Section titled “Introduction”AppBuilder is the composition root of a Xeno.JS application.
Use it to configure the application before startup:
- application services
- custom modules
- context
- middleware
- CQRS pipelines
- cache
- database
- authentication
- logging
- HTTP infrastructure
- concurrency services
The typical lifecycle is:
AppBuilder ↓configure ↓build() ↓ServiceContainer ↓application runtimeCreate an Application
Section titled “Create an Application”Create an AppBuilder and configure the capabilities your application needs:
import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addServices((services) => { services.addSingleton('GREETING_SERVICE', () => ({ greet: () => 'Hello Xeno!', }))})
const container = await app.build()
const greeting = container.resolve('GREETING_SERVICE')
console.log(greeting.greet())There are three main steps:
- Create the builder.
- Configure the application.
- Call
build().
Configuration methods return the same AppBuilder, so they can also be chained.
Register Services
Section titled “Register Services”Use addServices() to register application-specific dependencies:
const app = new AppBuilder()
app.addServices((services) => { services.addSingleton('USER_REPOSITORY', () => { return new UserRepository() })
services.addScoped('USER_SERVICE', (container) => { return new UserService( container.resolve('USER_REPOSITORY'), ) })})
await app.build()The callback receives the application’s ServiceContainer.
For service lifetimes and dependency registration, see the dependency injection documentation.
Add a Custom Module
Section titled “Add a Custom Module”Use addModule() when a feature needs to register several related services or perform its own configuration.
const app = new AppBuilder()
app.addModule( 'UsersModule', async () => ({ async configure(container) { container.addScoped('USER_SERVICE', () => { return new UserService() }) }, }),)
await app.build()The module factory is executed during build().
An optional configuration object can be passed as the third argument:
app.addModule( 'UsersModule', async () => ({ async configure(container, options) { container.addScoped('USER_SERVICE', () => { return new UserService(options) }) }, }), { enabled: true, },)Enable Context
Section titled “Enable Context”Context is enabled automatically when the builder is created.
You can therefore use:
const app = new AppBuilder()without explicitly calling addContext().
Calling addContext() is also safe and keeps the method available when composing application configuration explicitly:
const app = new AppBuilder()
app.addContext()Configure Middleware
Section titled “Configure Middleware”Use addMiddlewares() to configure Xeno.JS middleware:
const app = new AppBuilder()
app.addMiddlewares((options) => { options.cors = true options.withCredentials = true})
await app.build()Middleware configuration can also enable authentication-related behavior when authentication has been configured.
Automatic Dependencies
Section titled “Automatic Dependencies”Calling addMiddlewares() automatically enables the following capabilities when they have not already been queued:
- Context
- Logger
- Cache
You therefore do not need to manually call addLogger() or addCache() just to satisfy middleware dependencies.
If you want to configure them explicitly, configure them before or after addMiddlewares():
const app = new AppBuilder()
app.addLogger((options) => { options.console = true})
app.addCache((options) => { options.inMemory = true})
app.addMiddlewares((options) => { options.cors = true})Configure the CQRS Pipeline
Section titled “Configure the CQRS Pipeline”Use addPipeline() to enable the CQRS pipeline:
const app = new AppBuilder()
app.addPipeline((config) => { config.queryBus.isEnabled = true config.performance.thresholdMs = 100})
await app.build()The pipeline configuration includes:
- performance
- authorization
- validation
- command bus
- query bus
For example:
app.addPipeline((config) => { config.queryBus.isEnabled = true config.performance.thresholdMs = 100})Automatic Dependencies Injection
Section titled “Automatic Dependencies Injection”Calling addPipeline() automatically enables:
- Context
- Logger
- Cache
when they have not already been queued.
For example, this:
const app = new AppBuilder()
app.addPipeline((config) => { config.queryBus.isEnabled = true})also queues the default logger and cache configuration.
If you need custom logging or cache configuration, configure those capabilities explicitly:
const app = new AppBuilder()
app.addLogger((options) => { options.console = true})
app.addCache((options) => { options.inMemory = true})
app.addPipeline((config) => { config.queryBus.isEnabled = true})Configure the Cache
Section titled “Configure the Cache”Use addCache() to configure application caching:
const app = new AppBuilder()
app.addCache((options) => { options.inMemory = true})The default AppBuilder cache configuration is in-memory:
const app = new AppBuilder()
app.addCache()The cache can also be configured with Redis.
See Cache Overview and Redis Cache for the complete cache configuration.
Configure the Database
Section titled “Configure the Database”Use addDb() to configure the database:
const app = new AppBuilder()
app.addDb((options) => { options.connectionString = process.env.DATABASE_URL ?? ''})
await app.build()SQLite can be enabled through the same configuration:
app.addDb((options) => { options.connectionString = './database.db' options.enableSqlLite = true})The database is initialized during build().
Configure the database once at the application composition root and inject the required database services into application components.
Configure Authentication
Section titled “Configure Authentication”Use addAuth() to configure authentication:
const app = new AppBuilder()
app.addAuth((options, config) => { options.url = config.getOrThrow('SUPABASE_URL') options.key = config.getOrThrow('SUPABASE_KEY')})Authentication is initialized during build().
Configure Logging
Section titled “Configure Logging”Use addLogger() to configure logging:
const app = new AppBuilder()
app.addLogger((options) => { options.console = true})The logger configuration supports the logging providers exposed by Xeno.JS, including:
- Console
- Pino
- Sentry
- custom loggers
For example:
app.addLogger((options) => { options.console = true
options.pino.config = { level: 'info', }})See the observability documentation for provider-specific configuration.
Configure HTTP Infrastructure
Section titled “Configure HTTP Infrastructure”Use addHttpCore() to configure HTTP infrastructure:
const app = new AppBuilder()
app.addHttpCore((options, config) => { options.http.client.baseURL = config.get( 'API_BASE_URL', 'https://api.example.com', )
options.http.client.timeoutMs = 5000})HTTP configuration includes the HTTP client and resilience settings exposed by the current HttpCoreConfig.
HTTP is an infrastructure capability of Xeno.JS. It does not make HTTP the application boundary.
Configure an HTTP Adapter
Section titled “Configure an HTTP Adapter”Use addAdapter() to configure the transport adapter:
const app = new AppBuilder()
app.addAdapter((options) => { options.native = true})The current adapter configuration supports:
- native
- Vercel
- Fastify
- custom adapters
Configure the adapter that matches the transport used by the application.
Add the Concurrency Service
Section titled “Add the Concurrency Service”Use addConcurrencyService() when application code needs the Xeno.JS concurrency service:
const app = new AppBuilder()
app.addConcurrencyService()
await app.build()The service is registered in the application container and can then be resolved through its dependency token.
Resolve Services
Section titled “Resolve Services”AppBuilder exposes resolve() as a convenience for resolving a registered dependency:
const app = new AppBuilder()
app.addServices((services) => { services.addSingleton('GREETING_SERVICE', () => ({ greet: () => 'Hello Xeno!', }))})
await app.build()
const greeting = app.resolve('GREETING_SERVICE')
console.log(greeting.greet())You can also use the ServiceContainer returned by build():
const container = await app.build()
const greeting = container.resolve('GREETING_SERVICE')For application code, prefer dependency injection rather than repeatedly reaching back into AppBuilder.
Build the Application
Section titled “Build the Application”Call build() when configuration is complete:
const container = await app.build()build() initializes queued modules according to their priority and returns the configured ServiceContainer.
The important behavior is:
- Modules are ordered by priority.
- Each module is initialized.
- If initialization succeeds, the application is marked as built.
- The configured
ServiceContaineris returned.
If module initialization fails, build() throws a bootstrap error identifying the module that failed.
For example:
Bootstrap failed at [CacheModule]: ...The original error is preserved as the cause.
Build Only Once
Section titled “Build Only Once”AppBuilder keeps the built application container.
Calling build() again returns the same container:
const first = await app.build()const second = await app.build()
console.log(first === second)// trueApplication bootstrap should therefore normally happen once during application startup.
Module Ordering
Section titled “Module Ordering”AppBuilder queues modules with priorities so that required infrastructure is initialized before the components that depend on it.
The current built-in ordering includes:
| Priority | Module |
|---|---|
| 0 | Context |
| 1 | Logger |
| 2 | Cache |
| 3 | Authentication |
| 4 | Database / Middleware |
| 5 | CQRS |
| 30 | HTTP Core |
| 40 | Concurrency Service |
| 50 | Adapter / custom modules |
| 99 | Application services |
You normally do not need to manage these priorities yourself.
The important consequence is that application services registered through addServices() are initialized after the built-in infrastructure modules.
A Typical Composition Root
Section titled “A Typical Composition Root”A real application can combine the capabilities it needs:
import { AppBuilder } from '@xeno-js/core'
const app = new AppBuilder()
app.addLogger((options) => { options.console = true})
app.addCache()
app.addPipeline((config) => { config.queryBus.isEnabled = true})
app.addDb((options, config) => { options.connectionString = config.getOrThrow('DATABASE_URL')})
app.addServices((services) => { services.addScoped('USER_REPOSITORY', (container) => { return new UserRepository( container.resolve('DB_CONTEXT'), ) })
services.addScoped('USER_SERVICE', (container) => { return new UserService( container.resolve('USER_REPOSITORY'), ) })})
await app.build()The composition root now makes the application’s infrastructure and services explicit:
AppBuilder │ ├── Context ├── Logging ├── Cache ├── CQRS Pipeline ├── Database └── Application Services │ ▼ ServiceContainerAppBuilder and ServiceContainer
Section titled “AppBuilder and ServiceContainer”The two objects have different responsibilities:
| Component | Responsibility |
|---|---|
AppBuilder |
Compose and bootstrap the application |
ServiceContainer |
Register and resolve runtime dependencies |
Use AppBuilder during application composition:
const app = new AppBuilder()
app.addServices((services) => { services.addScoped('USER_SERVICE', () => { return new UserService() })})
await app.build()Use the container to resolve runtime dependencies:
const container = await app.build()
const service = container.resolve('USER_SERVICE')AppBuilder is therefore the composition API, while ServiceContainer is the runtime dependency mechanism.
AppBuilder and the Registry
Section titled “AppBuilder and the Registry”AppBuilder is generic over the application’s registry:
const app = new AppBuilder<MyRegistry>()The registry associates dependency tokens with their expected types.
This makes calls such as:
const logger = app.resolve(TOKENS.LOGGER)type-safe when TOKENS.LOGGER is part of the registry.
See Xeno Registry for the registry model.
When to Use AppBuilder
Section titled “When to Use AppBuilder”Use AppBuilder when defining the application’s composition root.
Typical responsibilities include:
- registering application services;
- enabling application modules;
- configuring infrastructure;
- configuring context;
- configuring middleware;
- configuring CQRS pipelines;
- configuring logging;
- configuring authentication;
- configuring databases;
- configuring caches;
- configuring HTTP infrastructure.
Do not use AppBuilder as a general-purpose runtime service locator.
Once the application is built, application components should receive their dependencies through dependency injection.
The Key Idea
Section titled “The Key Idea”AppBuilder answers one practical question:
How is this application composed?
The answer should be visible in one place:
const app = new AppBuilder()
app.addLogger(...)app.addCache(...)app.addPipeline(...)app.addDb(...)app.addServices(...)
await app.build()Configure the application’s capabilities and dependencies at the composition root, then hand execution over to the configured runtime
Section titled “Configure the application’s capabilities and dependencies at the composition root, then hand execution over to the configured runtime”Related Documentation
Section titled “Related Documentation”- Service Container
- Registration
- Resolution
- Xeno Registry
- CQRS Pipelines
- Cache Overview
- Redis Cache
- Observability Overview
Support Us
Section titled “Support Us”Xeno.JS is an MIT-licensed open source project. It can grow thanks to the support of these awesome people. If you’d like to join them, please read more at support section
