SpaceNode
SpaceNode's dependency injection is a simple, flat, key-value container with full lifecycle management. No decorators, no reflection, no magic.
*.service.js filesauthService)// modules/auth/auth.service.js
export const authService = { ... } // key: "authService"
// modules/users/users.service.js
export const userService = { ... } // key: "userService"
// In ANY controller from ANY module:
export function handler(request, services) {
const { send } = request
const { authService, userService } = services
// Both available – cross-module DI
}
Services can be plain objects, factory functions, or classes:
// Plain object — registered as singleton
export const cache = { store: new Map() }
// Factory function — called with container on first resolve
export const db = (container) => {
const config = container.resolve('config')
return new Database(config.dbUrl)
}
// Class instance — registered as singleton
export const logger = new Logger('app')
| Method | Description |
|---|---|
register(name, instanceOrFactory) | Register a service (plain value → singleton, function → singleton factory) |
registerAll(map) | Register multiple services at once |
singleton(name, factory) | Register a singleton factory — one shared instance, created lazily |
transient(name, factory) | Register a transient factory — new instance per resolve() call |
scoped(name, factory) | Register a scoped factory — one instance per scope (per-request) |
resolve(name) | Get a specific service by name (respects lifecycle) |
getAll() | Get all services (frozen, cached). This is injected as the handler's second argument |
has(name) | Check if a service is registered |
list() | Get all service names |
createScope() | Create a scoped child container (for per-request services) |
unregister(name) | Remove a service registration |
clear() | Clear all services (for testing) |
| Lifecycle | Behavior | Use case |
|---|---|---|
| Singleton | One shared instance for the entire app (default) | Database connections, caches, configs |
| Transient | New instance every time resolve() is called | Loggers, context-specific utilities |
| Scoped | One instance per scope (per-request via createScope()) | Per-request state, unit-of-work |
import { Container } from 'SpaceNode'
const c = new Container()
// Singleton — shared across the app
c.singleton('db', () => new Database())
// Transient — new instance every time
c.transient('requestLogger', () => new RequestLogger())
// Scoped — one per scope (per request)
c.scoped('unitOfWork', (container) => new UnitOfWork(container.resolve('db')))
// Create a scope for a request
const scope = c.createScope()
scope.resolve('unitOfWork') // → new UnitOfWork (unique to this scope)
scope.resolve('unitOfWork') // → same instance (within same scope)
scope.resolve('db') // → delegates to parent (singleton)
Created via container.createScope(). Scoped services get a per-scope instance. Singletons and transients delegate to the parent container.
| Method | Description |
|---|---|
resolve(name) | Resolve a service — scoped ones are per-scope, rest delegates to parent |
getAll() | Get all services (scoped instances resolved within this scope) |
has(name) | Check if a service exists (delegates to parent) |
list() | List all service names (delegates to parent) |
The container detects circular dependencies during factory resolution and throws a clear error with the dependency chain:
// This will throw:
c.singleton('a', (c) => c.resolve('b'))
c.singleton('b', (c) => c.resolve('a'))
c.resolve('a')
// Error: Circular dependency detected: a → b → a
| Step | Where | What happens |
|---|---|---|
| 1 | hello.service.js |
You export a named value: export const helloService = { ... } |
| 2 | loader.js → loadModule() |
Scans *.service.js, collects all named exports |
| 3 | app.js → container.register() |
Each export is registered by its name (function → singleton factory, object → singleton instance) |
| 4 | app.js → container.getAll() |
On each request, all services are resolved into one frozen object |
| 5 | handler(request, services) |
The frozen services object is passed as the second argument |
// In any controller, from any module:
export async function greetUser(request, services) {
const { body, send } = request
const { helloService } = services
const msg = helloService.greet(body.name)
send({ message: msg })
}
Key points:
helloService) becomes the DI key — no config neededimport, no manual registration, no decorators*.service.js, export a value — it just workscontainer as argument for dependency resolutiongetAll() is cached and returns a frozen object. The cache is invalidated only when a new service is registered or removed. Zero overhead per request.
The container supports removing services for testing or dynamic reconfiguration:
// Remove a single service
app.container.unregister('emailService')
// Remove ALL services (useful in test teardown)
app.container.clear()
After unregister() or clear(), the getAll() cache is invalidated so subsequent calls reflect the change.