ExisJS

Providers

Providers are a fundamental concept in ExisJS. Many of the basic classes may be treated as a provider: services, repositories, factories, helpers, and so on.

The main idea of a provider is that it can be injected as a dependency; this means objects can create various relationships with each other, and the function of "wiring up" instances of objects can largely be delegated to the ExisJS runtime system.

Services

Let's start by creating a simple UsersService. This service will be responsible for data storage and retrieval, and is designed to be used by the UsersController.

To mark a class as a provider that the ExisJS Inversion of Control (IoC) container can manage, we use the @Injectable() decorator.

src/http/users/service.ts
import { Injectable } from 'exisjs/decorators'
export interface User {  id: string;  email: string;}
@Injectable({ scope: 'singleton' })export class UsersService {  private readonly users: User[] = []
  create(user: User) {    this.users.push(user)  }
  findAll(): User[] {    return this.users  }}

Now that we have a service class to retrieve users, let's use it inside the UsersController. ExisJS provides a powerful inject() primitive that automatically resolves dependencies from the current context.

src/http/users/route.ts
import { Controller, Get, Post, Body } from 'exisjs/decorators'import { inject } from 'exisjs/di'import { UsersService, User } from './service'
@Controller('users')export default class UsersController {    // Clean, ergonomic dependency injection  private usersService = inject(UsersService)
  @Post()  async create(@Body() user: User) {    this.usersService.create(user)    return { success: true }  }
  @Get()  async findAll() {    return this.usersService.findAll()  }}
Note on Dependency Injection

ExisJS utilizes a runtime container system. By default, ExisJS does not aggressively use reflect-metadata for automatic constructor injection (unlike NestJS) to maintain a minimal footprint. The inject() function is the primary and recommended way to resolve dependencies.

Extended: Manual Resolution

If you are operating completely outside of an ExisJS controller or route (for example, in a background worker, CRON job, or a raw script like a database seeder), you can manually resolve dependencies as an escape hatch via the app container:

scripts/seed.ts
import { getActiveApp } from 'exisjs/app'import { UsersService } from './service'
const app = getActiveApp()const usersService = app.container.resolve(UsersService)

Custom Providers

ExisJS's DI container is flexible. By default, when you register a class, ExisJS instantiates it for you. However, you often need more control. You can register standard classes, raw values, factories, or aliased classes natively using app.provide().

Functional Paradigm

src/http/server.ts
import exis from 'exisjs'import { UsersService } from './users/service'
export default exis({  async onStart(app) {    // 1. Standard Class Provider    app.provide(UsersService, UsersService)
    // 2. Value Provider    app.provide('DATABASE_URL', {      useValue: 'postgres://localhost:5432/mydb'    })
    // 3. Factory Provider    app.provide('LoggerService', {      useFactory: () => {        return { log: (msg: string) => console.log(`[Logger]: ${msg}`) }      }    })
    // 4. Aliased Class Provider    app.provide('AuthService', {      useClass: UsersService    })  }})

Class-Based (OOP) Paradigm

src/http/server.ts
import { Server } from 'exisjs/decorators'import { UsersService } from './users/service'
@Server({  providers: [    // 1. Standard Class Provider    UsersService,    // 2. Value Provider    ['DATABASE_URL', { useValue: 'postgres://localhost:5432/mydb' }],    // 3. Factory Provider    ['LoggerService', { useFactory: () => ({ log: (msg: string) => console.log(`[Logger]: ${msg}`) }) }],    // 4. Aliased Class Provider    ['AuthService', { useClass: UsersService }]  ]})export default class RootServer {}

Injection Scopes

For people coming from different programming language backgrounds, it might be unexpected to learn that in ExisJS, almost everything is shared across incoming requests. We have a connection pool to the database, singleton services with global state, etc. This is because Node.js doesn't follow the request/response Multi-Threaded Stateless Model.

However, there are edge-cases when request-based lifetime may be the desired behavior (e.g., per-request caching in GraphQL, request tracking, multi-tenancy). ExisJS supports scopes directly in the @Injectable() decorator:

  • Singleton (Default): A single instance of the provider is shared across the entire application.
  • Request: A new instance of the provider is created exclusively for each incoming request, and garbage collected afterward.
DEFAULT

[!WARNING] Request Scope Constraints: A provider marked as scope: 'request' must be resolved within the lifecycle of an active HTTP request. If you try to inject() or resolve a request-scoped provider during application startup, inside a background queue worker, or from a cron job, the container will throw a runtime error.