ExisJS

Modules and Plugins

ExisJS provides modular architecture primitives to structure code into reusable, encapsulated domains. You can organize features using two approaches:

  1. Domain Modules (defineModule / @Module): Group related providers, sub-modules, controllers, and lifecycle hooks into domain boundaries.
  2. Framework Plugins (definePlugin): Build configurable, distributable extensions and third-party integrations with option passing.

Domain Modules Overview

Modules create logical boundaries around specific business domains (such as AuthModule, UsersModule, or BillingModule), managing their dependencies, routes, and lifecycles.

ConceptFunctional ParadigmClass-Based (OOP) Paradigm
Module DefinitiondefineModule({ name, imports, providers, routes, onStart, onClose })@Module({ imports, controllers, providers, exports })
Global Scopeapp.register(module) or exis.config.ts@Global() decorator on @Module() class
Provider Encapsulationproviders: [[token, definition]]providers: [Service, [token, definition], { provide, useValue }]

Functional Domain Modules (defineModule)

In the functional paradigm, use defineModule from exisjs/module to declare encapsulated domain units.

1. Defining a Functional Module

src/modules/auth.module.ts
import { defineModule } from 'exisjs/module'import { DatabaseModule } from './database.module'import { AuthService } from '../services/auth.service'
export const AuthModule = defineModule({  name: 'AuthModule',
  // 1. Sub-modules required by this module  imports: [DatabaseModule],
  // 2. Providers registered into the DI container  providers: [    ['AuthService', { useClass: AuthService }],  ],
  // 3. Optional lifecycle hooks  async onStart(app) {    console.log('[AuthModule] Initialized successfully')  },
  async onClose(app) {    console.log('[AuthModule] Teardown complete')  },})

2. Registering Modules in Server Configuration

You can register modules in exis.config.ts (using either defineConfig or typed const config: ExisConfig) or directly in src/http/server.ts:

In exis.config.ts

exis.config.ts
import { defineConfig } from 'exisjs/config'import { AuthModule } from './src/modules/auth.module'
export default defineConfig({  port: 4000,  plugins: [    AuthModule,  ],})

Alternatively, you can export a typed ExisConfig object directly:

exis.config.ts
import type { ExisConfig } from 'exisjs/config'import { AuthModule } from './src/modules/auth.module'
const config: ExisConfig = {  port: 4000,  plugins: [    AuthModule,  ],}
export default config

In src/http/server.ts

src/http/server.ts
import { exis } from 'exisjs'import { AuthModule } from '../modules/auth.module'
export default exis({  async onStart(app) {    await app.register(AuthModule)  },})

Class-Based (OOP) Modules (@Module & @Global)

In class-based architectures, import Module from exisjs/module (or exisjs/decorators) to configure imported modules, controllers, providers, and exported dependencies.

1. Module Definition with Controllers and Providers

src/modules/users.module.ts
import { Module } from 'exisjs/module'import { UsersController } from '../http/users/controller'import { UsersService } from '../services/users.service'import { DatabaseModule } from './database.module'
@Module({  imports: [DatabaseModule],  controllers: [UsersController],  providers: [UsersService],  exports: [UsersService],})export class UsersModule {}

2. Global Modules (@Global)

Utility providers, audit loggers, or database connections needed across all modules can be decorated with @Global(). Global modules make their exported providers available across the entire application without needing to be re-imported into every @Module({ imports: [...] }) declaration:

src/modules/core.module.ts
import { Module } from 'exisjs/module'import { Global } from 'exisjs/decorators'import { LoggerService } from '../services/logger.service'import { AuditService } from '../services/audit.service'
@Global()@Module({  providers: [LoggerService, AuditService],  exports: [LoggerService, AuditService],})export class CoreModule {}

3. Root Server Registration

Register root modules and global modules using the @Server decorator in src/http/server.ts:

src/http/server.ts
import { Server } from 'exisjs/decorators'import { CoreModule } from '../modules/core.module'import { UsersModule } from '../modules/users.module'import { AuthModule } from '../modules/auth.module'
@Server({  plugins: [CoreModule, UsersModule, AuthModule],})export default class RootServer {}

Framework Plugins (definePlugin)

When creating reusable plugins, libraries, or third-party integrations (e.g. Stripe, Redis, S3), use definePlugin from exisjs/plugin.

definePlugin returns a hybrid object that can be passed directly to app.register() without arguments or invoked as a function to receive configuration parameters.

1. Authoring a Configurable Plugin

src/plugins/stripe.plugin.ts
import { definePlugin } from 'exisjs/plugin'import Stripe from 'stripe'
export interface StripePluginOptions {  apiKey: string  apiVersion?: Stripe.LatestApiVersion}
export const StripePlugin = definePlugin<StripePluginOptions>({  name: 'stripe-plugin',
  async register(app, options) {    if (!options?.apiKey) {      throw new Error('[StripePlugin] "apiKey" is required')    }
    const client = new Stripe(options.apiKey, {      apiVersion: options.apiVersion || '2023-10-16',    })
    // Register provider into the application container    app.provide('StripeClient', { useValue: client })  },})

2. Registering Plugins with Configuration Options

Pass the plugin with custom options into exis.config.ts (via defineConfig or const config: ExisConfig) or via app.register():

exis.config.ts
import { defineConfig } from 'exisjs/config'import { StripePlugin } from './src/plugins/stripe.plugin'
export default defineConfig({  plugins: [    StripePlugin({      apiKey: process.env.STRIPE_SECRET_KEY!,    }),  ],})
src/http/server.ts
import { exis } from 'exisjs'import { StripePlugin } from '../plugins/stripe.plugin'
export default exis({  async onStart(app) {    await app.register(      StripePlugin({ apiKey: process.env.STRIPE_SECRET_KEY! })    )  },})

Module and Plugin Lifecycle Hooks

Both defineModule and definePlugin support application lifecycle coordination:

HookInvocation PhaseTypical Use Case
register(app, options)Server initialization during app.register()Registering DI providers, registering custom routes, configuring middleware.
onStart(app)Triggered after all plugins and routes are loaded, right before the server starts listeningEstablishing persistent background connections, pre-warming caches.
onClose(app)Triggered upon graceful shutdown signals (SIGINT, SIGTERM)Draining connection pools, flushing queues, terminating worker threads.