Core - Application, Service Providers & DI Container
The core module provides the Application lifecycle manager, the Service Provider pattern, the IoC container, and the @inject decorator. Together they form the backbone of Strav's dependency injection and bootstrap system.
The Container
The Container class manages service registration and resolution. Services can be registered as singletons (one shared instance) or transient (new instance per resolution).
import { Container } from '@strav/kernel'
const app = new Container()
Registering services
// Singleton — same instance every time
app.singleton(Database)
app.singleton(Logger)
app.singleton(UserService)
// Transient — new instance every time
app.register(RequestContext)
// String-keyed with factory function
app.singleton('mailer', (container) => {
const config = container.resolve(Configuration)
return new Mailer(config.get('mail.host'))
})
// Chaining
const app = new Container()
.singleton(Database)
.singleton(Logger)
.singleton(UserService)
Resolving services
// By class
const db = app.resolve(Database)
// By string name
const mailer = app.resolve('mailer')
// Check existence
app.has(Database) // true
app.has('missing') // false
Auto-wiring
When a class is decorated with @inject, the container reads its constructor parameter types via reflect-metadata and automatically resolves them. No manual wiring needed:
@inject
class UserService {
constructor(
private db: Database, // auto-resolved
private logger: Logger, // auto-resolved
) {}
}
app.singleton(Database)
app.singleton(Logger)
app.singleton(UserService)
const svc = app.resolve(UserService) // db and logger are injected
The @inject decorator
Mark any class with @inject to make it eligible for auto-wiring:
import { inject } from '@strav/kernel'
@inject
class PaymentService {
constructor(private db: Database) {}
}
The decorator works both as @inject and @inject(). It sets internal metadata that the container checks during resolution.
The global app container
A pre-created singleton container is exported for convenience:
import { app } from '@strav/kernel'
app.singleton(Database)
const db = app.resolve(Database)
Instantiating without registration
make() creates an instance with full DI auto-wiring but without requiring prior registration. Dependencies are resolved recursively: registered services come from the container, unregistered @inject classes are instantiated via make() as well.
@inject
class NotificationService {
constructor(private mailer: Mailer) {}
}
// Mailer is registered, NotificationService is not
app.singleton(Mailer)
// make() resolves Mailer from the container, instantiates NotificationService
const svc = app.make(NotificationService)
This is used internally by router.resource() and [Controller, 'method'] tuples to resolve controllers without explicit registration.
Key rules
- All constructor dependencies must themselves be registered in the container (or be
make()-able) before the dependent service is resolved. - Singleton instances are created lazily — on first
resolve(), not onsingleton(). - Circular dependencies are not handled — they will cause a stack overflow.
Application
The Application class extends Container with service provider lifecycle management. It is the primary way to bootstrap a Strav application — registering providers, booting them in dependency order, and handling graceful shutdown.
import { app } from '@strav/kernel'
import {
ConfigProvider, EncryptionProvider, StorageProvider,
CacheProvider, I18nProvider, LoggerProvider,
} from '@strav/kernel'
import { DatabaseProvider } from '@strav/database'
import { AuthProvider, SessionProvider, HttpProvider } from '@strav/http'
import { MailProvider, NotificationProvider, BroadcastProvider } from '@strav/signal'
import { QueueProvider } from '@strav/queue'
import User from './app/models/user'
app
.use(new ConfigProvider())
.use(new DatabaseProvider())
.use(new AuthProvider({ resolver: (id) => User.find(id) }))
.use(new SessionProvider())
.use(new CacheProvider())
.use(new MailProvider())
.use(new QueueProvider())
.use(new HttpProvider())
await app.start()
// Server is running. Graceful shutdown on SIGINT/SIGTERM is automatic.
Since Application extends Container, all DI methods (singleton, resolve, register, make, has) continue to work unchanged.
Registration order doesn't matter
Providers declare their dependencies via the dependencies property. The application uses topological sort (Kahn's algorithm) to boot them in the correct order — regardless of the order you call use():
// These two are equivalent:
app.use(new AuthProvider()).use(new DatabaseProvider()).use(new ConfigProvider())
app.use(new ConfigProvider()).use(new DatabaseProvider()).use(new AuthProvider())
// Both boot in order: config → database → auth
Lifecycle
app.start() runs two phases in dependency order:
- Register — calls
provider.register(app)on all providers (synchronous, binds factories into the container) - Boot — calls
provider.boot(app)on all providers (async, initializes services)
If a provider's boot() throws, all previously booted providers are shut down in reverse order (rollback), and the error is re-thrown.
Graceful shutdown
app.start() installs SIGINT and SIGTERM signal handlers automatically. On signal:
app.shutdown()is called- Providers are shut down in reverse boot order (e.g., HTTP server stops first, database closes last)
- A 30-second timeout forces exit if providers don't finish
Lifecycle events are emitted via Emitter:
| Event | When |
|---|---|
app:starting |
Before the register phase |
app:booted |
After all providers are booted |
app:shutdown |
Shutdown initiated |
app:terminated |
After all providers are shut down |
Application API
app.use(provider) // add a provider (before start)
await app.start() // register + boot all providers
await app.shutdown() // graceful shutdown (reverse order)
app.isBooted // true after start() completes
app.isShuttingDown // true during shutdown
Service Providers
A service provider encapsulates the full lifecycle of a framework service: registration (binding into the container), booting (async initialization), and shutdown (cleanup).
import { ServiceProvider } from '@strav/kernel'
import type { Application } from '@strav/kernel'
Anatomy of a provider
class MyProvider extends ServiceProvider {
readonly name = 'my-service' // unique identifier
readonly dependencies = ['config'] // boot after these providers
register(app: Application): void {
app.singleton(MyService) // bind into container
}
async boot(app: Application): Promise {
const svc = app.resolve(MyService) // resolve and initialize
await svc.connect()
}
async shutdown(app: Application): Promise {
const svc = app.resolve(MyService)
await svc.disconnect() // clean up
}
}
| Method | Phase | Description |
|---|---|---|
register() |
Synchronous | Bind factories/singletons into the container |
boot() |
Async | Resolve services, run async initialization (load config, create tables, connect) |
shutdown() |
Async | Clean up resources (close connections, stop servers) |
Writing a custom provider
Extend ServiceProvider, set name and optionally dependencies, and implement the lifecycle methods you need:
import { ServiceProvider } from '@strav/kernel'
import type { Application } from '@strav/kernel'
class RedisProvider extends ServiceProvider {
readonly name = 'redis'
readonly dependencies = ['config']
register(app: Application): void {
app.singleton('redis', () => new Redis(app.resolve(Configuration).get('redis')))
}
boot(app: Application): void {
app.resolve('redis') // trigger lazy creation
}
async shutdown(app: Application): Promise {
await app.resolve('redis').quit()
}
}
Built-in providers
Strav's providers are distributed across multiple packages for modularity. Here's where to find each provider:
| Provider | Package | Name | Dependencies | What it does |
|---|---|---|---|---|
ConfigProvider |
@strav/kernel |
config |
— | Loads Configuration from config directory |
DatabaseProvider |
@strav/database |
database |
config |
Registers Database, closes on shutdown |
EncryptionProvider |
@strav/kernel |
encryption |
config |
Registers EncryptionManager |
LoggerProvider |
@strav/kernel |
logger |
config |
Registers Logger |
CacheProvider |
@strav/kernel |
cache |
config |
Registers CacheManager |
StorageProvider |
@strav/kernel |
storage |
config |
Registers StorageManager |
AuthProvider |
@strav/http |
auth |
database |
Registers Auth, sets user resolver, creates tables |
SessionProvider |
@strav/http |
session |
database |
Registers SessionManager, creates table |
MailProvider |
@strav/signal |
mail |
config |
Registers MailManager |
QueueProvider |
@strav/queue |
queue |
database |
Registers Queue, creates tables |
NotificationProvider |
@strav/signal |
notification |
database |
Registers NotificationManager, creates table |
I18nProvider |
@strav/kernel |
i18n |
config |
Registers I18nManager, loads translations |
BroadcastProvider |
@strav/signal |
broadcast |
— | Boots BroadcastManager on the router |
HttpProvider |
@strav/http |
http |
config |
Registers Server, starts HTTP server, stops on shutdown |
External packages provide their own providers:
| Provider | Package | Name | Dependencies |
|---|---|---|---|
SearchProvider |
@strav/search |
search |
config |
DevtoolsProvider |
@strav/devtools |
devtools |
database |
BrainProvider |
@strav/brain |
brain |
config |
StripeProvider |
@strav/stripe |
stripe |
database |
SocialProvider |
@strav/social |
social |
database |
Provider options
Providers that need user input accept options in their constructor:
new ConfigProvider({ directory: './config' })
new AuthProvider({ resolver: (id) => User.find(id), ensureTables: true })
new SessionProvider({ ensureTable: true })
new QueueProvider({ ensureTables: true })
new NotificationProvider({ ensureTable: true })
new BroadcastProvider({ middleware: [session()], path: '/_broadcast' })
Full bootstrap example
// index.ts
import { app } from '@strav/kernel'
import { router, session, auth } from '@strav/http'
// Core providers from @strav/kernel
import {
ConfigProvider, EncryptionProvider, LoggerProvider,
CacheProvider, StorageProvider, I18nProvider,
} from '@strav/kernel'
// Database provider
import { DatabaseProvider } from '@strav/database'
// HTTP-related providers
import { AuthProvider, SessionProvider, HttpProvider } from '@strav/http'
// Communication providers
import { MailProvider, NotificationProvider, BroadcastProvider } from '@strav/signal'
// Queue provider
import { QueueProvider } from '@strav/queue'
// Optional providers
import { SearchProvider } from '@strav/search'
import { DevtoolsProvider } from '@strav/devtools'
import User from './app/models/user'
// Register providers
app
.use(new ConfigProvider())
.use(new DatabaseProvider())
.use(new EncryptionProvider())
.use(new LoggerProvider())
.use(new CacheProvider())
.use(new StorageProvider())
.use(new AuthProvider({ resolver: (id) => User.find(id) }))
.use(new SessionProvider())
.use(new MailProvider())
.use(new QueueProvider())
.use(new NotificationProvider())
.use(new I18nProvider())
.use(new BroadcastProvider({ middleware: [session()] }))
.use(new SearchProvider())
.use(new DevtoolsProvider())
.use(new HttpProvider())
// Define routes
router.get('/health', (ctx) => ctx.json({ status: 'ok' }))
router.group({ prefix: '/api', middleware: [session(), auth()] }, (r) => {
r.get('/users', listUsers)
})
// Boot everything
await app.start()