Modules and Plugins
ExisJS provides modular architecture primitives to structure code into reusable, encapsulated domains. You can organize features using two approaches:
- Domain Modules (
defineModule/@Module): Group related providers, sub-modules, controllers, and lifecycle hooks into domain boundaries. - 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.
| Concept | Functional Paradigm | Class-Based (OOP) Paradigm |
|---|---|---|
| Module Definition | defineModule({ name, imports, providers, routes, onStart, onClose }) | @Module({ imports, controllers, providers, exports }) |
| Global Scope | app.register(module) or exis.config.ts | @Global() decorator on @Module() class |
| Provider Encapsulation | providers: [[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
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
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:
import type { ExisConfig } from 'exisjs/config'import { AuthModule } from './src/modules/auth.module'
const config: ExisConfig = { port: 4000, plugins: [ AuthModule, ],}
export default configIn 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
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:
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:
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
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():
import { defineConfig } from 'exisjs/config'import { StripePlugin } from './src/plugins/stripe.plugin'
export default defineConfig({ plugins: [ StripePlugin({ apiKey: process.env.STRIPE_SECRET_KEY!, }), ],})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:
| Hook | Invocation Phase | Typical 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 listening | Establishing persistent background connections, pre-warming caches. |
onClose(app) | Triggered upon graceful shutdown signals (SIGINT, SIGTERM) | Draining connection pools, flushing queues, terminating worker threads. |