JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 9
  • Score
    100M100P100Q50155F
  • License MIT

Simple, unified NestJS integration for Inngest with multi-platform support and type safety

Package Exports

  • nestjs-inngest
  • nestjs-inngest/dist/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (nestjs-inngest) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

NestJS Inngest Integration

๐Ÿš€ Simple, unified NestJS integration for Inngest with multi-platform support and type safety.

npm version License: MIT TypeScript

โœจ Features

Core Features

  • ๐Ÿš€ Seamless NestJS Integration - Native dependency injection and NestJS patterns
  • โšก Multi-Platform Support - Works with both Express and Fastify HTTP platforms
  • ๐Ÿ”’ Type Safety - Full TypeScript support with typed event definitions and handlers
  • ๐ŸŽฏ Decorator-Based - Simple @InngestFunction decorator for defining serverless functions
  • ๐Ÿ”„ Automatic Discovery - Zero-config function registration and discovery
  • ๐ŸŽ›๏ธ Unified API - Single createServe() method works with both platforms
  • ๐ŸŽฎ Controller Integration - Custom NestJS controller support with createControllerHandler()

Developer Experience

  • ๐Ÿ”ง Simplified Configuration - Streamlined module setup with essential options only
  • ๐Ÿ“ Error Handling - Comprehensive error reporting with detailed validation messages
  • ๐Ÿ› Debug Logging - Enhanced logging for development and troubleshooting
  • ๐Ÿงช Basic Testing - Simple testing utilities for unit and integration tests
  • ๐ŸŽฏ Flexible Integration - Choose between automatic middleware/plugin or custom controller integration

Quick Start

1. Module Setup

First, configure the InngestModule in your NestJS application:

import { Module } from "@nestjs/common";
import { InngestModule } from "nestjs-inngest";

@Module({
  imports: [
    InngestModule.forRoot({
      appId: "my-nestjs-app",
      // signingKey and eventKey not required in local, but required in Inngest-Cloud or your self-host, you need add NODE_ENV === "development" into .env file
      signingKey: process.env.INNGEST_SIGNING_KEY, // Inngest authenticates to me when calling my functions
      eventKey: process.env.INNGEST_EVENT_KEY, // I authenticate to Inngest when sending events
    }),
  ],
})
export class AppModule {}

2. HTTP Platform Setup

This module supports both Express and Fastify HTTP platforms with a unified API. You can choose between automatic integration (middleware/plugin) or controller-based integration for more control.

import { NestFactory } from "@nestjs/core";
import { NestExpressApplication } from "@nestjs/platform-express";
import {
  NestFastifyApplication,
  FastifyAdapter,
} from "@nestjs/platform-fastify";
import { InngestService } from "nestjs-inngest";
import { AppModule } from "./app.module";

async function bootstrap() {
  // Platform switch - change this to switch platforms
  const USE_FASTIFY = false; // Set to true for Fastify, false for Express

  if (USE_FASTIFY) {
    // Create Fastify application
    const app = await NestFactory.create<NestFastifyApplication>(
      AppModule,
      new FastifyAdapter()
    );

    // Setup Inngest Fastify plugin
    const inngestService = app.get(InngestService);
    const { plugin, options } = await inngestService.createServe("fastify");
    await app.register(plugin, options);

    await app.listen(3000, "0.0.0.0");
  } else {
    // Create Express application
    const app = await NestFactory.create<NestExpressApplication>(AppModule, {
      bodyParser: false,
    });

    // Configure Express body parser
    app.useBodyParser("json", { limit: "10mb" });

    // Setup Inngest Express middleware
    const inngestService = app.get(InngestService);
    const serveMiddleware = await inngestService.createServe("express");
    app.use("/api/inngest", serveMiddleware);

    await app.listen(3000);
  }
}
bootstrap();

Option B: Controller-Based Integration

For more control over the Inngest webhook endpoint, you can use a custom NestJS controller:

Express Controller Example:

// express-inngest.controller.ts
import { Controller, Post, Put, Get, Req, Res, Logger } from "@nestjs/common";
import { InngestService } from "nestjs-inngest";

@Controller("api/inngest")
export class ExpressInngestController {
  private readonly logger = new Logger(ExpressInngestController.name);

  constructor(private readonly inngestService: InngestService) {}

  @Get()
  async handleIntrospection(
    @Req() req: any,
    @Res({ passthrough: false }) res: any
  ) {
    return this.handleInngestRequest(req, res, "GET");
  }

  @Post()
  async handleWebhook(@Req() req: any, @Res({ passthrough: false }) res: any) {
    return this.handleInngestRequest(req, res, "POST");
  }

  @Put()
  async handlePutWebhook(
    @Req() req: any,
    @Res({ passthrough: false }) res: any
  ) {
    return this.handleInngestRequest(req, res, "PUT");
  }

  private async handleInngestRequest(req: any, res: any, method: string) {
    try {
      // Use the controller-specific handler for Express
      const controllerHandler =
        await this.inngestService.createControllerHandler("express");
      return controllerHandler(req, res);
    } catch (error) {
      this.logger.error("Error in Inngest controller:", error);
      return res.status(500).json({ error: "Internal Server Error" });
    }
  }
}

Fastify Controller Example:

// fastify-inngest.controller.ts
import { Controller, Post, Put, Get, Req, Res, Logger } from "@nestjs/common";
import { InngestService } from "nestjs-inngest";
import type { FastifyRequest, FastifyReply } from "fastify";

@Controller("api/inngest")
export class FastifyInngestController {
  private readonly logger = new Logger(FastifyInngestController.name);

  constructor(private readonly inngestService: InngestService) {}

  @Get()
  async handleIntrospection(
    @Req() req: FastifyRequest,
    @Res({ passthrough: false }) res: FastifyReply
  ) {
    return this.handleInngestRequest(req, res, "GET");
  }

  @Post()
  async handleWebhook(
    @Req() req: FastifyRequest,
    @Res({ passthrough: false }) res: FastifyReply
  ) {
    return this.handleInngestRequest(req, res, "POST");
  }

  @Put()
  async handlePutWebhook(
    @Req() req: FastifyRequest,
    @Res({ passthrough: false }) res: FastifyReply
  ) {
    return this.handleInngestRequest(req, res, "PUT");
  }

  private async handleInngestRequest(
    req: FastifyRequest,
    res: FastifyReply,
    method: string
  ) {
    try {
      // Use the controller-specific handler for Fastify
      const controllerHandler =
        await this.inngestService.createControllerHandler("fastify");
      return await controllerHandler(req, res);
    } catch (error) {
      this.logger.error("Error in Fastify Inngest controller:", error);
      return res.status(500).send({
        error: "Internal Server Error",
        message: error.message,
      });
    }
  }
}

Main Application (Fastify with Controller):

// main.ts
import { NestFactory } from "@nestjs/core";
import { FastifyAdapter } from "@nestjs/platform-fastify";
import type { NestFastifyApplication } from "@nestjs/platform-fastify";
import { AppModule } from "./app.module";

async function bootstrap() {
  // Create Fastify application
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter()
  );

  await app.listen(3000, "0.0.0.0");
  console.log(
    "๐Ÿš€ Fastify app with Inngest controller running on http://localhost:3000"
  );
  console.log("๐ŸŽฏ Inngest webhook: http://localhost:3000/api/inngest");
}
bootstrap();

Don't forget to add the controller to your module:

// app.module.ts
import { Module } from "@nestjs/common";
import { InngestModule } from "nestjs-inngest";
import { FastifyInngestController } from "./fastify-inngest.controller";

@Module({
  imports: [
    InngestModule.forRoot({
      appId: "my-fastify-app",
      signingKey: process.env.INNGEST_SIGNING_KEY,
      eventKey: process.env.INNGEST_EVENT_KEY,
    }),
  ],
  controllers: [FastifyInngestController],
})
export class AppModule {}

Integration Method Comparison

Feature Automatic Integration Controller Integration
Setup Complexity โœ… Simple ๐ŸŸก Medium
Control Level ๐ŸŸก Standard โœ… Full Control
Custom Logic โŒ Limited โœ… Full Support
Error Handling ๐ŸŸก Basic โœ… Custom
Middleware Support โœ… Built-in โœ… NestJS Decorators
Best For Quick setup, standard use cases Custom logic, advanced error handling

Note: Both createServe() and createControllerHandler() methods provide a unified API that works with both Express and Fastify platforms!

Create type-safe event definitions:

import { EventTypes } from "nestjs-inngest";

export type MyEventTypes = EventTypes<{
  "user.created": { userId: string; email: string; name: string };
  "order.placed": { orderId: string; userId: string; total: number };
  "email.send": { to: string; template: string; data: any };
}>;

4. Create Inngest Functions

Use the @InngestFunction decorator to define serverless functions:

import { Injectable } from "@nestjs/common";
import { InngestFunction, InngestFunctionContext } from "nestjs-inngest";

@Injectable()
export class UserService {
  @InngestFunction({
    id: "user-welcome-flow",
    name: "User Welcome Flow",
    triggers: [{ event: "user.created" }],
  })
  async handleUserCreated(
    event: any,
    { step, logger, runId, attempt }: InngestFunctionContext
  ) {
    const { userId, email, name } = event.data;

    // Step 1: Create user profile
    const profile = await step.run("create-profile", async () => {
      return this.createUserProfile(userId, { email, name });
    });

    // Step 2: Send welcome email
    await step.run("send-welcome-email", async () => {
      return this.sendEmail({
        to: email,
        template: "welcome",
        data: { name, userId },
      });
    });

    // Step 3: Send event notification
    await step.sendEvent("send-notification", {
      name: "user.notification",
      data: { userId, type: "welcome", email },
    });

    return { success: true, userId, profileId: profile.id };
  }

  private async createUserProfile(userId: string, data: any) {
    // Your implementation
  }

  private async sendEmail(params: any) {
    // Your implementation
  }
}

5. Send Events

Inject the InngestService to send events:

import { Injectable } from "@nestjs/common";
import { InngestService } from "nestjs-inngest";

@Injectable()
export class AuthService {
  constructor(private readonly inngestService: InngestService) {}

  async createUser(userData: any) {
    // Create user in database
    const user = await this.userRepository.save(userData);

    // Trigger Inngest function
    await this.inngestService.send({
      name: "user.created",
      data: {
        userId: user.id,
        email: user.email,
        name: user.name,
      },
    });

    return user;
  }
}

Installation

npm install nestjs-inngest inngest
# or
yarn add nestjs-inngest inngest
# or
pnpm add nestjs-inngest inngest

Configuration

Basic Configuration

InngestModule.forRoot({
  appId: "my-app", // Required: Your app identifier
  signingKey: "your-key", // Required: For webhook verification
  eventKey: "your-event-key", // Required: For sending events
  endpoint: "/api/inngest", // Optional: Webhook endpoint path
  env: "production", // Optional: Environment
  timeout: 30000, // Optional: Function timeout in ms
});

Async Configuration

InngestModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    appId: configService.get("INNGEST_APP_ID"),
    signingKey: configService.get("INNGEST_SIGNING_KEY"),
    eventKey: configService.get("INNGEST_EVENT_KEY"),
    env: configService.get("NODE_ENV"),
  }),
  inject: [ConfigService],
});

Configuration Options

Complete Parameter Reference

// Synchronous configuration
InngestModule.forRoot({
  // === REQUIRED PARAMETERS ===
  appId: "my-app", // ๐Ÿ”ต Inngest: Your app identifier

  // === INNGEST CORE PARAMETERS ===
  signingKey: process.env.INNGEST_SIGNING_KEY, // ๐Ÿ”ต Inngest: Webhook signature verification
  eventKey: process.env.INNGEST_EVENT_KEY, // ๐Ÿ”ต Inngest: Event sending authentication
  baseUrl: "https://api.inngest.com", // ๐Ÿ”ต Inngest: API base URL (defaults to Inngest cloud)
  isDev: process.env.NODE_ENV === "development", // ๐Ÿ”ต Inngest: Development mode flag

  // === CONNECTION METHODS ===
  enableConnect: false, // ๐ŸŸ  Extension: Use connect mode vs serve mode (default: false)

  // === HTTP & ROUTING ===
  endpoint: "/api/inngest", // ๐ŸŸ  Extension: Webhook endpoint path (default: "/api/inngest")

  // === PERFORMANCE & LIMITS ===
  timeout: 30000, // ๐Ÿ”ต Inngest: Function timeout in ms (default: 30000)
  maxBatchSize: 100, // ๐ŸŸ  Extension: Max events per batch (default: 100)

  // === DEVELOPMENT & DEBUGGING ===
  logger: true, // ๐ŸŸ  Extension: Enable detailed logging (default: true)
  env: "development", // ๐ŸŸ  Extension: Environment setting ("production"|"development"|"test")
  strict: false, // ๐ŸŸ  Extension: Enhanced validation (default: false)

  // === RETRY CONFIGURATION ===
  retry: {
    maxAttempts: 3, // ๐ŸŸ  Extension: Maximum retry attempts (default: 3)
    initialDelay: 1000, // ๐ŸŸ  Extension: Initial delay between retries in ms (default: 1000)
    maxDelay: 30000, // ๐ŸŸ  Extension: Maximum delay between retries in ms (default: 30000)
    backoff: "exponential", // ๐ŸŸ  Extension: Backoff strategy ("exponential"|"linear"|"fixed")
    backoffMultiplier: 2, // ๐ŸŸ  Extension: Backoff multiplier (default: 2)
  },

  // === DEVELOPMENT MODE (Advanced) ===
  development: {
    enabled: true, // ๐ŸŸ  Extension: Enable development features
    mockExternalCalls: false, // ๐ŸŸ  Extension: Mock external service calls
    localWebhookUrl: "http://localhost:3000", // ๐ŸŸ  Extension: Custom local webhook URL
    disableSignatureVerification: true, // ๐ŸŸ  Extension: Skip signature validation
    enableIntrospection: true, // ๐ŸŸ  Extension: Function debugging tools
    autoRegisterFunctions: true, // ๐ŸŸ  Extension: Auto-discover functions
    developmentTimeout: 60000, // ๐ŸŸ  Extension: Extended timeout for debugging
    enableStepDebugging: true, // ๐ŸŸ  Extension: Step-by-step execution logs
  },
});
// Inngest SDK
if (process.env.NODE_ENV === "development" || process.env.INNGEST_DEV === "1") {
  // auto disabled
  signatureVerification = false;
}

// Asynchronous configuration with environment variables
InngestModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    appId: configService.get("INNGEST_APP_ID"),
    signingKey: configService.get("INNGEST_SIGNING_KEY"),
    eventKey: configService.get("INNGEST_EVENT_KEY"),
    isDev: configService.get("NODE_ENV") === "development",
    enableConnect: configService.get("INNGEST_USE_CONNECT") === "true",
    // ... other parameters
  }),
  inject: [ConfigService],
});

Parameter Categories

๐Ÿ”ต Inngest Core Parameters - Native Inngest SDK parameters:

  • appId, signingKey, eventKey, baseUrl, isDev, timeout

๐ŸŸ  NestJS Extension Parameters - Our enhancements for better NestJS integration:

  • Connection management: enableConnect, endpoint
  • Development tools: logger, env, strict, development.*
  • Performance: maxBatchSize, retry.*

Quick Examples

Minimal Setup:

InngestModule.forRoot({
  appId: "my-app",
  // signingKey and eventKey not required in local, but required in Inngest-Cloud or your self-host, you need add NODE_ENV === "development" into .env file
  signingKey: process.env.INNGEST_SIGNING_KEY, // Inngest authenticates to me when calling my functions
  eventKey: process.env.INNGEST_EVENT_KEY, // I authenticate to Inngest when sending events
});

Development Setup:

InngestModule.forRoot({
  appId: "my-dev-app",
  isDev: true,
  development: {
    enabled: true,
    disableSignatureVerification: true,
  },
});

Production Setup:

InngestModule.forRoot({
  appId: "my-prod-app",
  signingKey: process.env.INNGEST_SIGNING_KEY,
  eventKey: process.env.INNGEST_EVENT_KEY,
  env: "production",
  logger: false,
  strict: true,
  retry: { maxAttempts: 5 },
});

Step Functions

Inngest provides step functions for reliable, resumable workflows:

@InngestFunction({
  id: 'user-workflow',
  triggers: [{ event: 'user.created' }],
})
async handleUserWorkflow(event: any, { step }: any) {
  // Steps are automatically retried on failure
  const profile = await step.run('create-profile', async () => {
    return this.createUserProfile(event.data);
  });

  // Sleep/delay execution
  await step.sleep('wait-before-email', '1 minute');

  // Send events to trigger other functions
  await step.sendEvent('send-welcome-email', {
    name: 'email.send',
    data: { userId: profile.id, email: event.data.email }
  });

  return { success: true, profileId: profile.id };
}

Type Safety (Optional)

For applications requiring strict type safety, use the typed decorator with your event schema:

import { TypedInngestFunction, EventRegistry } from "nestjs-inngest";

// Define your event types
interface MyEventRegistry extends EventRegistry {
  "user.created": { userId: string; email: string; name: string };
  "order.completed": { orderId: string; amount: number; userId: string };
  "email.sent": { to: string; subject: string; success: boolean };
}

@Injectable()
export class UserService {
  @TypedInngestFunction<MyEventRegistry, "user.created">({
    id: "user-welcome-flow",
    triggers: [{ event: "user.created" }],
    config: {
      retries: 3,
      timeout: 30000,
      priority: 1,
    },
  })
  async handleUserCreated({
    event,
    step,
  }: TypedEventContext<MyEventRegistry, "user.created">) {
    // event.data is strictly typed as { userId: string; email: string; name: string }
    const { userId, email, name } = event.data;

    return await step.run("create-profile", async () => {
      return this.createUserProfile({ userId, email, name });
    });
  }
}

Advanced Type Helpers

Create event-specific decorators:

import { createEventDecorator, CronFunction } from "nestjs-inngest";

// Create a user-created specific decorator
const UserCreatedFunction = createEventDecorator<MyEventRegistry, 'user.created'>('user.created');

// Use the specific decorator
@UserCreatedFunction({
  id: "send-welcome-email",
  name: "Send Welcome Email"
})
async sendWelcomeEmail({ event }: TypedEventContext<MyEventRegistry, 'user.created'>) {
  // Automatically typed for user.created events
}

// Cron-based functions
@CronFunction({
  id: "daily-cleanup",
  cron: "0 2 * * *", // 2 AM daily
  timezone: "UTC"
})
async dailyCleanup() {
  // Scheduled function
}

Performance Optimization (Optional)

For applications with many functions or high-performance requirements, you can use the optimized decorator:

import { OptimizedInngestFunction } from "nestjs-inngest";

@Injectable()
export class UserService {
  @OptimizedInngestFunction({
    id: "user-welcome-flow",
    triggers: [{ event: "user.created" }],
  })
  async handleUserCreated(event: any, { step }: any) {
    // Same functionality as @InngestFunction, but with:
    // - Multi-layer caching for faster metadata processing
    // - Memory optimization with object pooling
    // - Batch validation and lookup capabilities
    // - Performance monitoring and statistics

    return await step.run("process-user", async () => {
      return this.processUser(event.data);
    });
  }
}

Performance Monitoring

Monitor decorator performance in high-load scenarios:

import {
  getDecoratorPerformanceStats,
  clearOptimizedCaches,
} from "nestjs-inngest";

// Get performance statistics
const stats = getDecoratorPerformanceStats();
console.log({
  registeredClasses: stats.registeredClasses,
  totalFunctions: stats.totalFunctions,
  cacheHitRate: stats.cacheHitRate,
  memoryUsage: stats.memoryUsage,
});

// Clear caches when needed (e.g., during testing)
clearOptimizedCaches();

Decorator Comparison

Feature @InngestFunction @TypedInngestFunction @OptimizedInngestFunction @CronFunction
Type Safety โŒ Basic (any) โœ… Strict TypeScript โŒ Basic (any) โœ… Typed Config
Performance ๐ŸŸข Standard ๐ŸŸข Standard โœ… Optimized ๐ŸŸข Standard
Event Validation ๐ŸŸก Runtime only โœ… Compile + Runtime ๐ŸŸก Runtime only โœ… Compile Time
Memory Usage ๐ŸŸข Low ๐ŸŸข Low ๐ŸŸก Higher (caching) ๐ŸŸข Low
Complexity ๐ŸŸข Simple ๐ŸŸก Medium ๐ŸŸก Medium ๐ŸŸข Simple
IDE Support ๐ŸŸก Basic โœ… Full IntelliSense ๐ŸŸก Basic โœ… Full IntelliSense
Trigger Types โœ… Event + Cron โœ… Event + Cron โœ… Event + Cron ๐ŸŽฏ Cron Only
Best For General use Type-safe apps High-performance apps Scheduled tasks

When to Use Each Decorator

@InngestFunction (Recommended for most cases)

  • โœ… General purpose applications
  • โœ… Quick prototyping and development
  • โœ… Simple event handling
  • โœ… When type safety is not critical

@TypedInngestFunction (For type-safe applications)

  • โœ… Large applications with complex event schemas
  • โœ… Teams requiring strict type safety
  • โœ… When you want compile-time event validation
  • โœ… Better IDE support and IntelliSense

@OptimizedInngestFunction (For high-performance scenarios)

  • โœ… Applications with 50+ Inngest functions
  • โœ… High-frequency function registration/discovery
  • โœ… Performance-critical environments
  • โœ… When you need performance monitoring

@CronFunction (For scheduled tasks)

  • โœ… Pure cron-based scheduled tasks
  • โœ… When you want clean, explicit cron syntax
  • โœ… Type-safe cron configuration with timezone support
  • โœ… Priority and timeout configuration for scheduled jobs
  • โœ… Cleaner than manually configuring cron triggers

CronFunction Examples

import { CronFunction } from "nestjs-inngest";

@Injectable()
export class ScheduledTasksService {
  // Daily cleanup at 2 AM UTC
  @CronFunction({
    id: "daily-cleanup",
    name: "Daily Cleanup Task",
    cron: "0 2 * * *",
    timezone: "UTC",
    retries: 2,
    timeout: 300000, // 5 minutes
    priority: 1, // High priority
  })
  async dailyCleanup() {
    // Cleanup logic here
    return { cleaned: true, timestamp: new Date() };
  }

  // Weekly report every Monday at 9 AM
  @CronFunction({
    id: "weekly-report",
    cron: "0 9 * * 1", // Monday 9 AM
    timezone: "America/New_York",
  })
  async weeklyReport() {
    // Generate weekly report
  }

  // High-frequency monitoring every 30 minutes
  @CronFunction({
    id: "health-check",
    cron: "*/30 * * * *", // Every 30 minutes
    timeout: 5000,
    retries: 1,
  })
  async healthCheck() {
    // Health monitoring logic
  }
}

Testing

Mock the InngestService for unit testing:

const mockInngestService = {
  send: jest.fn(),
  getClient: jest.fn(),
};

// Use in your test modules
providers: [{ provide: InngestService, useValue: mockInngestService }];

Key Concepts

  • @InngestFunction: Standard decorator for general-purpose event handling
  • @TypedInngestFunction: Type-safe decorator with strict TypeScript event validation
  • @OptimizedInngestFunction: Performance-optimized decorator for high-load applications
  • InngestService: Main service for sending events and creating integrations
    • send() - Send events to Inngest
    • getClient() - Get underlying Inngest client
    • createServe(platform) - Create middleware/plugin integration
    • createControllerHandler(platform) - Create controller-based integration
  • Step Functions: Use step.run(), step.sleep(), step.sendEvent() for reliable workflows
  • Events: Send events with { name: string, data: any } structure

Troubleshooting

  • Functions not registering: Ensure services are imported in your module and decorated with @Injectable()
  • Webhook errors: Verify signingKey configuration and endpoint URL
  • Debug mode: Set logger: true and isDev: true for detailed logging

Planned for Future Versions

  • Performance optimizations (connection pooling, memory management, request optimization)
  • Enhanced testing utilities and mocks
  • Typed function decorators (@TypedInngestFunction)
  • Advanced logging and monitoring
  • Circuit breakers and resilient error handling
  • Event sourcing and audit trails

License

MIT License - see CHANGELOG.md for version history.