Skip to content

Dependency Injection ​

Moost uses dependency injection (DI) to manage object creation and wiring. Every controller is automatically injectable. This page covers scopes, injection patterns, and how scopes unlock property-level resolvers.

Scopes ​

Every injectable class has a scope that controls its lifecycle:

ScopeBehaviorDefault
SINGLETONOne instance shared across all requestsYes
FOR_EVENTNew instance created for each requestNo

SINGLETON (default) ​

Controllers are singletons by default — one instance handles all requests. This is efficient and works well when handlers don't store per-request state:

ts
@Controller('users')
export class UserController {
    @Get(':id')
    find(@Param('id') id: string) {
        return { id } // id comes from parameters, not instance state
    }
}

FOR_EVENT ​

Use @Injectable('FOR_EVENT') when you need per-request state on the controller instance. This is particularly useful for property-level resolvers:

ts
import { Get } from '@moostjs/event-http'
import { Controller, Injectable, Param } from 'moost'

@Injectable('FOR_EVENT')
@Controller('greet')
export class GreetController {
    @Param('name')
    name!: string

    @Get(':name')
    hello() {
        return `Hello, ${this.name}!` // resolved per-request
    }
}

Because a new GreetController is created for each request, this.name safely holds the current request's :name parameter.

Property-level resolvers ​

In FOR_EVENT controllers, resolver decorators (@Param, @Header, @Cookie, @Body, etc.) can be used on class properties instead of handler arguments:

ts
import { Get, Post, Header, Body, Cookie } from '@moostjs/event-http'
import { Controller, Injectable, Param } from 'moost'

@Injectable('FOR_EVENT')
@Controller('api')
export class ApiController {
    @Param('id')
    id!: string

    @Header('authorization')
    authHeader!: string

    @Cookie('session')
    sessionToken!: string

    @Get('items/:id')
    getItem() {
        // all properties are resolved before the handler runs
        return { id: this.id, auth: this.authHeader }
    }
}

INFO

@Query is the exception — it is typed as a parameter decorator only, so use it in handler signatures, not on class properties.

See Custom Resolvers & Pipes for building your own property/parameter resolvers.

Response refs as properties ​

FOR_EVENT scope makes response refs especially elegant. Instead of using ref objects in handler parameters, bind them to properties:

ts
import { Get, StatusRef, HeaderRef, CookieRef, CookieAttrsRef } from '@moostjs/event-http'
import type { TCookieAttributes } from '@moostjs/event-http'
import { Controller, Injectable } from 'moost'

@Injectable('FOR_EVENT')
@Controller()
export class ResponseController {
    @StatusRef()
    status = 200

    @HeaderRef('x-custom')
    customHeader = ''

    @CookieRef('session')
    sessionCookie = ''

    @CookieAttrsRef('session')
    sessionAttrs: TCookieAttributes = { httpOnly: true }

    @Get('example')
    example() {
        this.status = 201
        this.customHeader = 'some-value'
        this.sessionCookie = 'new-token'
        this.sessionAttrs = { maxAge: '1h', httpOnly: true }
        return { ok: true }
    }
}

Assigning to this.status, this.customHeader, etc. directly sets the response status, headers, and cookies. The initial values serve as defaults.

Constructor injection ​

Any @Injectable() class can be injected through constructor parameters:

ts
import { Injectable } from 'moost'

@Injectable()
export class UserService {
    findById(id: string) {
        return { id, name: 'John' }
    }
}
ts
import { Controller, Param } from 'moost'
import { Get } from '@moostjs/event-http'
import { UserService } from './user.service'

@Controller('users')
export class UserController {
    constructor(private userService: UserService) {}

    @Get(':id')
    find(@Param('id') id: string) {
        return this.userService.findById(id)
    }
}

Moost resolves UserService automatically when creating UserController.

@Provide and @Inject ​

For interface-based injection or when you need custom factories:

ts
import { Provide, Inject, Controller } from 'moost'
import { Get } from '@moostjs/event-http'

// Register a factory for a token
@Provide('CONFIG', () => ({ apiUrl: 'https://api.example.com' }))
@Controller()
export class AppController {
    constructor(@Inject('CONFIG') private config: { apiUrl: string }) {}

    @Get('config')
    getConfig() {
        return this.config
    }
}

Token registration scopes, app-level provide(), and replacement patterns are covered in depth in Provide & Inject.

Scope rules ​

WARNING

A SINGLETON class cannot depend on a FOR_EVENT class. The singleton is created once, so it can't receive a new instance per request.

A FOR_EVENT class can depend on a SINGLETON class — it just receives the shared instance.

SINGLETON → SINGLETON    ✅
FOR_EVENT → SINGLETON    ✅
FOR_EVENT → FOR_EVENT    ✅
SINGLETON → FOR_EVENT    ❌ will not work correctly

When in doubt, keep your controllers as singletons and use handler parameters for request-specific data. Switch to FOR_EVENT only when property-level resolvers or per-request state provide a clear benefit.

Further reading ​

Released under the MIT License.