ExisJS

Modules & Plugins

ExisJS provides a modular architecture. The framework is built on a Plugin Engine that allows you to encapsulate functionality, inject dependencies, and distribute reusable packages across your organization.

ExisJS offers two distinct primitives for extension:

  1. Modules (defineModule): Used for grouping domain-level business logic (e.g., AuthModule, UsersModule).
  2. Plugins (definePlugin): Used for building framework-level extensions that often require configuration options (e.g., StripePlugin({ apiKey })).

1. Domain Modules (defineModule)

When building large-scale applications, you should avoid placing all your logic into a single file. Modules allow you to encapsulate related dependencies (Providers), sub-modules (Imports), and Routes into a single unit.

Defining a Module

Let's build an AuthModule that encapsulates an authentication provider:

src/modules/auth.module.ts
import { defineModule } from 'exisjs/module'
export const AuthModule = defineModule({  name: 'auth-module',    // 1. Inject Dependencies  providers: [    ['AuthService', { useClass: class AuthService { login() {} } }]  ],
  // 2. Import other modules  imports: [    DatabaseModule  ],
  // 3. Register lifecycle hooks  async onStart(app) {    console.log("AuthModule has initialized")  }})

Registering Modules

Once your module is defined, you can register it globally. The recommended place to register core modules is inside your exis.config.ts file, ensuring they boot up synchronously before your server starts accepting traffic.

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

Alternatively, you can register it manually inside your src/http/server.ts:

Functional Paradigm

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) Paradigm

src/http/server.ts
import { Server } from 'exisjs/decorators'import { AuthModule } from '../modules/auth.module'import type { App } from 'exisjs'
@Server({  plugins: [AuthModule]})export default class RootServer {  async onStart(app: App) {    console.log("Server with AuthModule booted")  }}

2. Framework Plugins (definePlugin)

While defineModule is great for internal business logic, definePlugin is designed for building distributable, third-party packages that require configuration options.

When you use definePlugin, ExisJS returns a Hybrid Object. This means the plugin can be registered directly if it doesn't need options, or it can be called as a function if you need to pass a configuration object.

Building a Plugin

Let's build a StripePlugin that requires an API key:

src/plugins/stripe.plugin.ts
import { definePlugin } from 'exisjs/plugin'
interface StripeOptions {  apiKey: string}
export const StripePlugin = definePlugin<StripeOptions>({  name: 'stripe-plugin',    async register(app, options) {    if (!options?.apiKey) {      throw new Error("Stripe API Key is required")    }
    // Initialize Stripe client and inject it globally    const stripeClient = new Stripe(options.apiKey)        app.provide('StripeClient', { useValue: stripeClient })  }})

Using the Plugin

Because we used definePlugin, the framework allows us to pass configuration options directly:

exis.config.ts
import { defineExisConfig } from 'exisjs/config'import { StripePlugin } from './src/plugins/stripe.plugin'import { env } from './src/env'
export default defineExisConfig({  plugins: [    // We call the plugin as a function to pass options    StripePlugin({ apiKey: env.STRIPE_SECRET_KEY })  ]})