code examples

Sent logo
Sent TeamMay 3, 2025 / code examples / Article

Building Scalable Marketing Campaigns with NestJS and AWS SNS

A guide on integrating AWS Simple Notification Service (SNS) with a NestJS application to build a scalable system for publishing marketing messages.

Unlock the power of asynchronous communication and massive scalability for your marketing campaigns by integrating AWS Simple Notification Service (SNS) with your NestJS application. This guide provides a complete, step-by-step walkthrough for building a robust system capable of publishing marketing messages efficiently to a large number of subscribers.

We will build a NestJS backend service with an API endpoint that accepts campaign details and publishes them to an AWS SNS topic. Subscribers to this topic (e.g., email endpoints, SMS numbers, SQS queues connected to other microservices) will then receive the campaign message. This decouples your core application from the delivery mechanism, enabling high throughput and resilience.

Technologies Used:

  • Node.js: Runtime environment.
  • NestJS: Progressive Node.js framework for building efficient, reliable server-side applications.
  • TypeScript: Superset of JavaScript adding static types.
  • AWS SNS: Fully managed pub/sub messaging service for decoupling microservices, distributed systems, and serverless applications.
  • AWS SDK for JavaScript v3: Modern AWS SDK for interacting with services like SNS.
  • Dotenv & @nestjs/config: For managing environment variables securely.

System Architecture:

+-----------------+ +-------------------+ +-----------------+ +----------------------+ | Client (e.g., |----->| NestJS API |----->| AWS SNS Topic |----->| Subscribers (Email, | | Admin UI, CLI) | POST | (Campaign Service)| pub | (Marketing | sub | SMS, SQS, Lambda, etc)| +-----------------+ +-------------------+ +-----------------+ +----------------------+ | - Validate Request | | - Use AWS SDK | | - Publish to SNS | +-------------------+

Prerequisites:

  • Node.js (LTS version recommended) and npm/yarn installed.
  • An AWS account with permissions to manage SNS and IAM.
  • AWS CLI installed and configured locally (for initial setup/testing is helpful).
  • Basic understanding of NestJS concepts (modules, controllers, services, dependency injection).
  • Familiarity with basic terminal/command-line operations.
  • curl or a tool like Postman for testing API endpoints.

Final Outcome:

By the end of this guide, you will have a functional NestJS application capable of receiving marketing campaign requests via a REST API endpoint and publishing those campaigns as messages to an AWS SNS topic for broad distribution.


1. Setting Up the NestJS Project

Let's start by creating a new NestJS project and installing the necessary dependencies.

  1. Create a New NestJS Project: Open your terminal and run the NestJS CLI command:

    bash
    npx @nestjs/cli new nestjs-sns-marketing
    cd nestjs-sns-marketing

    Choose your preferred package manager (npm or yarn) when prompted.

  2. Install Dependencies: We need the AWS SDK v3 client for SNS and NestJS configuration module.

    bash
    # Using npm
    npm install @aws-sdk/client-sns @nestjs/config dotenv class-validator class-transformer
    
    # Using yarn
    yarn add @aws-sdk/client-sns @nestjs/config dotenv class-validator class-transformer
    • @aws-sdk/client-sns: The AWS SDK for JavaScript v3 module specifically for interacting with SNS.
    • @nestjs/config: For loading and accessing environment variables (like AWS credentials and SNS topic ARN).
    • dotenv: To load environment variables from a .env file during development.
    • class-validator & class-transformer: For request data validation using DTOs.
  3. Configure Environment Variables: Create a .env file in the root directory of your project (nestjs-sns-marketing/.env). This file will store sensitive information and configuration details. Never commit this file to version control.

    dotenv
    # .env
    
    # AWS Credentials (Ensure the IAM user/role has sns:Publish permissions)
    AWS_ACCESS_KEY_ID=YOUR_AWS_ACCESS_KEY_ID
    AWS_SECRET_ACCESS_KEY=YOUR_AWS_SECRET_ACCESS_KEY
    AWS_REGION=us-east-1 # Replace with your desired AWS region
    
    # AWS SNS Configuration
    SNS_MARKETING_TOPIC_ARN=YOUR_SNS_TOPIC_ARN # ARN of the SNS topic you'll create
    
    # Application Port (Optional)
    PORT=3000
    
    # Database Path (Optional, if using Section 6)
    # DATABASE_PATH=marketing_campaigns.sqlite
    • AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY: Obtain these by creating an IAM user in your AWS account with programmatic access and the necessary SNS permissions (specifically sns:Publish for the target topic). Follow AWS best practices for managing credentials securely. For production deployments in AWS environments (like EC2, ECS, Lambda, Fargate), always prefer using IAM roles attached to the compute resource over storing long-lived AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY as environment variables. IAM roles provide temporary, automatically rotated credentials, significantly improving security.
    • AWS_REGION: The AWS region where your SNS topic resides (e.g., us-west-2, eu-central-1).
    • SNS_MARKETING_TOPIC_ARN: The Amazon Resource Name (ARN) of the SNS topic you will create in the next step. It uniquely identifies the topic.
    • PORT: The port your NestJS application will listen on.
  4. Load Environment Variables Globally: Modify src/app.module.ts to import and configure ConfigModule globally. This makes environment variables accessible throughout the application via ConfigService.

    typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule } from '@nestjs/config'; // Import ConfigModule
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { CampaignModule } from './campaign/campaign.module'; // We'll create this next
    
    @Module({
      imports: [
        ConfigModule.forRoot({
          isGlobal: true, // Make ConfigService available globally
          envFilePath: '.env', // Specify the env file path
        }),
        CampaignModule, // Add CampaignModule here
        // Add TypeOrmModule here if using Section 6
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
  5. Project Structure: NestJS CLI generates a standard structure. We will add a campaign module to encapsulate our marketing campaign functionality.

    nestjs-sns-marketing/ ├── src/ │ ├── campaign/ # Module for campaign features │ │ ├── campaign.module.ts │ │ ├── campaign.controller.ts │ │ ├── campaign.service.ts │ │ ├── dto/ # Data Transfer Objects for validation │ │ │ └── send-campaign.dto.ts │ │ └── entities/ # (Optional) Database entities │ │ └── campaign.entity.ts │ ├── app.controller.ts │ ├── app.module.ts │ ├── app.service.ts │ └── main.ts ├── .env # Environment variables (DO NOT COMMIT) ├── .gitignore ├── nest-cli.json ├── package.json ├── tsconfig.build.json ├── tsconfig.json └── # other config files...

2. Setting Up AWS SNS Topic

Before implementing the NestJS code, you need an SNS topic to publish messages to.

  1. Log in to AWS Management Console: Navigate to the AWS Management Console.
  2. Navigate to SNS: Find and select Simple Notification Service (SNS) under Application Integration services.
  3. Go to Topics: In the SNS dashboard navigation pane, click ""Topics"".
  4. Create Topic:
    • Click the ""Create topic"" button.
    • Type: Choose ""Standard"". Standard topics offer higher throughput and best-effort ordering, suitable for marketing campaigns where strict order isn't usually critical. FIFO topics guarantee order and exactly-once delivery but have lower throughput limits.
    • Name: Enter a descriptive name, e.g., marketing-campaigns.
    • Display name (optional): A shorter name used in SMS messages.
    • Leave other settings as default for now (Access policy, Encryption, etc.) unless you have specific security requirements. You can refine the access policy later to restrict who can publish/subscribe.
    • Click ""Create topic"".
  5. Copy Topic ARN: Once created, find your topic in the list. Click on its name to view details. Copy the ARN (Amazon Resource Name) displayed prominently. It will look something like: arn:aws:sns:us-east-1:123456789012:marketing-campaigns.
  6. Update .env File: Paste the copied ARN into your .env file for the SNS_MARKETING_TOPIC_ARN variable.

Important IAM Permissions: Ensure the AWS credentials (access key/secret or IAM role) used by your application have at least the sns:Publish permission for the specific Topic ARN you just created. You can attach a policy like this to the user/role:

json
{
  ""Version"": ""2012-10-17"",
  ""Statement"": [
    {
      ""Effect"": ""Allow"",
      ""Action"": ""sns:Publish"",
      ""Resource"": ""YOUR_SNS_TOPIC_ARN""
    }
  ]
}

(Replace YOUR_SNS_TOPIC_ARN with your actual Topic ARN)


3. Implementing Core Functionality (SNS Publisher Service)

Now, let's create the service responsible for interacting with AWS SNS.

  1. Generate the Campaign Module, Controller, and Service: Use the NestJS CLI to scaffold the necessary components:

    bash
    npx nest g module campaign
    npx nest g controller campaign --no-spec # No spec files for brevity
    npx nest g service campaign --no-spec   # No spec files for brevity

    This creates the src/campaign directory and its basic files. Remember to add CampaignModule to the imports array in src/app.module.ts (as shown in Step 1.4).

  2. Create the SNS Publisher Service (campaign.service.ts): This service will encapsulate the logic for publishing messages to SNS using the AWS SDK v3.

    typescript
    // src/campaign/campaign.service.ts
    import { Injectable, Logger } from '@nestjs/common';
    import { ConfigService } from '@nestjs/config';
    import {
      SNSClient,
      PublishCommand,
      PublishCommandInput,
      PublishCommandOutput,
    } from '@aws-sdk/client-sns';
    
    @Injectable()
    export class CampaignService {
      private readonly logger = new Logger(CampaignService.name);
      private readonly snsClient: SNSClient;
      private readonly topicArn: string;
    
      constructor(private configService: ConfigService) {
        // Initialize SNS Client in the constructor
        // Reads region and credentials from ConfigService (environment variables)
        // Ensure AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_REGION are set in .env
        // or that the application is running with an IAM role in an AWS environment.
        this.snsClient = new SNSClient({
          region: this.configService.getOrThrow<string>('AWS_REGION'),
          // Credentials will be automatically sourced from environment variables,
          // shared credential file (~/.aws/credentials), or IAM role if available.
          // Explicitly providing keys from ConfigService is also an option:
          // credentials: {
          //   accessKeyId: this.configService.getOrThrow<string>('AWS_ACCESS_KEY_ID'),
          //   secretAccessKey: this.configService.getOrThrow<string>('AWS_SECRET_ACCESS_KEY'),
          // },
        });
    
        // Get Topic ARN from environment variables
        this.topicArn = this.configService.getOrThrow<string>('SNS_MARKETING_TOPIC_ARN');
        this.logger.log(`SNS Publisher initialized for topic: ${this.topicArn}`);
      }
    
      /**
       * Publishes a message (campaign) to the configured AWS SNS topic.
       * @param subject The subject line for the message (especially useful for email subscribers).
       * @param messageBody The main content of the message/campaign.
       * @returns The PublishCommandOutput from AWS SNS, containing the MessageId.
       * @throws Error if publishing fails.
       */
      async publishCampaign(subject: string, messageBody: string): Promise<PublishCommandOutput> {
        const params: PublishCommandInput = {
          TopicArn: this.topicArn,
          Subject: subject, // Subject is used by email subscriptions
          Message: messageBody, // The actual message content
          // Optional: Add MessageAttributes for filtering or structuring
          // MessageAttributes: {
          //   campaignType: { DataType: 'String', StringValue: 'promotion' },
          // },
        };
    
        this.logger.log(`Publishing campaign "${subject}" to ${this.topicArn}`);
    
        try {
          const command = new PublishCommand(params);
          const response = await this.snsClient.send(command);
          this.logger.log(`Message published successfully: ${response.MessageId}`);
          return response;
        } catch (error) {
          this.logger.error(`Failed to publish message to SNS: ${error.message}`, error.stack);
          // Re-throw the error or handle it appropriately for your application
          // Consider mapping specific AWS errors to NestJS HttpException for better API responses
          throw new Error(`Failed to publish campaign: ${error.message}`);
        }
      }
    }

    Explanation:

    • Dependencies: We inject ConfigService to access environment variables.
    • Logger: A NestJS Logger instance for logging informative messages and errors.
    • SNS Client Initialization: The SNSClient from @aws-sdk/client-sns is instantiated in the constructor. It automatically picks up credentials from standard sources (environment variables, shared files, IAM roles). The AWS_REGION is explicitly set from ConfigService. getOrThrow ensures the application fails fast if essential variables like AWS_REGION or SNS_MARKETING_TOPIC_ARN are missing.
    • Topic ARN: The target SNS_MARKETING_TOPIC_ARN is also fetched and stored.
    • publishCampaign Method:
      • Takes subject and messageBody as input.
      • Constructs the PublishCommandInput object required by the AWS SDK. TopicArn and Message are mandatory. Subject is particularly useful for email subscribers.
      • Uses SNSClient.send() with a PublishCommand to send the message. This is the modern asynchronous way to interact with the SDK v3.
      • Logs success with the returned MessageId or logs and throws an error on failure.
    • Why this approach? Encapsulating SNS logic in a dedicated service follows the principle of separation of concerns, making the code modular and testable. Using the AWS SDK v3 aligns with current AWS best practices.

4. Building the API Layer (Campaign Controller)

Now, create the API endpoint that will receive requests to send campaigns and utilize the CampaignService.

  1. Create Data Transfer Object (DTO) for Validation: It's crucial to validate incoming request data. We'll use NestJS's built-in validation pipes along with the class-validator library (installed in Step 1.2).

    Create the DTO file: src/campaign/dto/send-campaign.dto.ts

    typescript
    // src/campaign/dto/send-campaign.dto.ts
    import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
    
    export class SendCampaignDto {
      @IsString()
      @IsNotEmpty()
      @MaxLength(100) // SNS subject limit is 100 chars
      subject: string;
    
      @IsString()
      @IsNotEmpty()
      @MaxLength(262144) // SNS message size limit (256 KB) - adjust if needed
      messageBody: string;
    }
    • This DTO defines the expected shape of the request body (subject and messageBody).
    • Decorators (@IsString, @IsNotEmpty, @MaxLength) provide validation rules.
  2. Enable Validation Pipe Globally: Enable automatic request validation by configuring ValidationPipe in src/main.ts.

    typescript
    // src/main.ts
    import { NestFactory } from '@nestjs/core';
    import { AppModule } from './app.module';
    import { ValidationPipe, Logger } from '@nestjs/common'; // Import ValidationPipe & Logger
    import { ConfigService } from '@nestjs/config'; // Import ConfigService
    
    async function bootstrap() {
      const app = await NestFactory.create(AppModule);
      const configService = app.get(ConfigService); // Get ConfigService instance
      const port = configService.get<number>('PORT', 3000); // Get port from env or default
      const logger = new Logger('Bootstrap'); // Create logger instance
    
      // Enable global validation pipe
      app.useGlobalPipes(
        new ValidationPipe({
          whitelist: true, // Strip away properties not defined in DTO
          forbidNonWhitelisted: true, // Throw error if extra properties are sent
          transform: true, // Automatically transform payloads to DTO instances
        }),
      );
    
      await app.listen(port);
      logger.log(`Application listening on port ${port} - ${new Date().toISOString()}`);
    }
    bootstrap();
  3. Implement the Campaign Controller (campaign.controller.ts): This controller defines the /campaigns route and handles incoming POST requests.

    typescript
    // src/campaign/campaign.controller.ts
    import { Controller, Post, Body, HttpCode, HttpStatus, Logger, HttpException } from '@nestjs/common';
    import { CampaignService } from './campaign.service';
    import { SendCampaignDto } from './dto/send-campaign.dto';
    
    @Controller('campaigns') // Route prefix: /campaigns
    export class CampaignController {
      private readonly logger = new Logger(CampaignController.name);
    
      constructor(private readonly campaignService: CampaignService) {}
    
      @Post('send') // Route: POST /campaigns/send
      @HttpCode(HttpStatus.ACCEPTED) // Return 202 Accepted on success
      async sendCampaign(@Body() sendCampaignDto: SendCampaignDto) {
        this.logger.log(
          `Received request to send campaign: Subject - ""${sendCampaignDto.subject}""`,
        );
    
        try {
          const result = await this.campaignService.publishCampaign(
            sendCampaignDto.subject,
            sendCampaignDto.messageBody,
          );
    
          // Basic response indicating acceptance and providing the SNS Message ID
          return {
            status: 'Campaign submitted for publishing',
            messageId: result.MessageId,
          };
          // If using the database approach from Section 6, you might return:
          // return {
          //   status: 'Campaign submitted for publishing',
          //   campaignId: result.campaignId, // ID from your database
          //   messageId: result.messageId,   // ID from SNS
          // };
    
        } catch (error) {
          this.logger.error(`Error processing send campaign request: ${error.message}`, error.stack);
          // Improve error handling: Map specific errors (e.g., AWS SDK errors)
          // to appropriate HTTP status codes using HttpException or custom Exception Filters.
          // For now, re-throwing will likely result in a 500 Internal Server Error.
          if (error instanceof Error) {
              throw new HttpException(
                  `Failed to process campaign: ${error.message}`,
                  HttpStatus.INTERNAL_SERVER_ERROR
              );
          }
          // Fallback for non-Error types
          throw new HttpException(
              'An unexpected error occurred while processing the campaign.',
              HttpStatus.INTERNAL_SERVER_ERROR
          );
        }
      }
    }

    Explanation:

    • @Controller('campaigns'): Defines the base route for this controller.
    • constructor: Injects the CampaignService.
    • @Post('send'): Defines a handler for POST requests to /campaigns/send.
    • @Body(): Decorator that extracts the request body. Combined with the ValidationPipe (enabled globally in main.ts), it automatically validates the incoming data against SendCampaignDto.
    • @HttpCode(HttpStatus.ACCEPTED): Sets the default success status code to 202 Accepted. This is appropriate because publishing to SNS is asynchronous; we've accepted the request for processing, but delivery isn't guaranteed synchronously.
    • Logic: Calls the campaignService.publishCampaign method with data from the validated DTO.
    • Response: Returns a success message including the MessageId received from SNS (or potentially the database campaignId if using Section 6).
    • Error Handling: Catches errors from the service call. The example now uses HttpException to return a 500 status code with a more informative message, but a custom Exception Filter is recommended for production to handle different error types gracefully.

5. Error Handling, Logging, and Retry Mechanisms

  • Error Handling Strategy:
    • Validation Errors: Handled automatically by ValidationPipe, returning 400 Bad Request responses with details.
    • SNS Publishing Errors: Caught within CampaignService, logged with details (including stack trace), and re-thrown. The controller catches this and can return an appropriate HTTP error (e.g., 500 Internal Server Error via HttpException or a custom filter). For production, implement a custom Exception Filter (@Catch()) to map specific errors (e.g., AWS SDK throttling errors like ThrottlingException) to appropriate HTTP status codes (like 429 Too Many Requests or 503 Service Unavailable) and provide user-friendly error messages.
    • Configuration Errors: ConfigService.getOrThrow in CampaignService ensures the app fails on startup if essential environment variables are missing.
  • Logging:
    • NestJS's built-in Logger is used in the service, controller, and main.ts.
    • Logs provide context (e.g., "Received request", "Publishing campaign", "Message published successfully", error details). Include correlation IDs (like campaignId if using Section 6, or a generated request ID) for easier tracing.
    • In production, configure logging levels (e.g., only log INFO and above) and consider structured logging (JSON format) for easier parsing by log aggregation tools (like CloudWatch Logs, Datadog, Splunk). NestJS allows custom logger implementations.
  • Retry Mechanisms (SNS):
    • Client-Side: The AWS SDK v3 has built-in retry mechanisms with exponential backoff for transient network errors or throttled requests (like ProvisionedThroughputExceededException or ThrottlingException). This is configurable when creating the SNSClient but defaults are usually reasonable.
    • Server-Side (SNS): SNS itself provides delivery retries for certain destination types (like HTTP/S endpoints) if they fail to acknowledge receipt. This doesn't apply directly to publishing to SNS but is relevant for subscribers.
    • Application-Level Retries: For critical campaigns, if the initial snsClient.send() fails with a potentially retryable error (e.g., temporary service unavailability not handled by the SDK's retries), you could implement a custom retry loop within your CampaignService using libraries like async-retry, but rely on the SDK's built-in retries first.

Example: Testing an Error Scenario

  1. Temporarily change SNS_MARKETING_TOPIC_ARN in your .env to an invalid ARN (e.g., append -invalid).
  2. Restart the application (npm run start:dev).
  3. Send a valid POST request to /campaigns/send.
  4. Observe the application logs – you should see the "Failed to publish message to SNS" error logged by CampaignService.
  5. Observe the API response – you should receive a 500 Internal Server Error (or the response defined by your controller's error handling/exception filter).

6. Database Schema and Data Layer (Optional)

While not strictly required for publishing campaigns, you might want to store campaign metadata or track publishing status. Here's a minimal example using TypeORM and SQLite.

  1. Install Database Dependencies:

    bash
    # Using npm
    npm install @nestjs/typeorm typeorm sqlite3
    
    # Using yarn
    yarn add @nestjs/typeorm typeorm sqlite3
  2. Create Campaign Entity (src/campaign/entities/campaign.entity.ts):

    typescript
    // src/campaign/entities/campaign.entity.ts
    import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, Index } from 'typeorm';
    
    export enum CampaignStatus {
      PENDING = 'PENDING',
      SUBMITTED = 'SUBMITTED', // Submitted to SNS
      FAILED = 'FAILED',
    }
    
    @Entity('campaigns') // Maps to the 'campaigns' table
    export class Campaign {
      @PrimaryGeneratedColumn('uuid')
      id: string;
    
      @Column({ length: 100 }) // Match DTO validation
      subject: string;
    
      @Column('text') // Use 'text' for potentially long messages
      messageBody: string;
    
      @Index() // Index status for faster lookups
      @Column({
        type: 'varchar', // Use varchar for enums with TypeORM/SQLite
        enum: CampaignStatus,
        default: CampaignStatus.PENDING,
      })
      status: CampaignStatus;
    
      @Index() // Index SNS message ID if you need to query by it
      @Column({ nullable: true })
      snsMessageId?: string; // Store the SNS Message ID if successful
    
      @CreateDateColumn()
      createdAt: Date;
    
      @Column({ type: 'text', nullable: true }) // Store failure reason
      failureReason?: string;
    }
  3. Configure TypeORM in AppModule (src/app.module.ts):

    typescript
    // src/app.module.ts
    import { Module } from '@nestjs/common';
    import { ConfigModule, ConfigService } from '@nestjs/config';
    import { TypeOrmModule } from '@nestjs/typeorm'; // Import TypeOrmModule
    import { AppController } from './app.controller';
    import { AppService } from './app.service';
    import { CampaignModule } from './campaign/campaign.module';
    import { Campaign } from './campaign/entities/campaign.entity'; // Import Campaign entity
    
    @Module({
      imports: [
        ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
        TypeOrmModule.forRootAsync({ // Use forRootAsync for ConfigService injection
          imports: [ConfigModule], // Make ConfigService available
          useFactory: (configService: ConfigService) => ({
            type: 'sqlite',
            database: configService.get<string>('DATABASE_PATH', 'marketing_campaigns.sqlite'), // Use env var or default
            entities: [Campaign], // Register Campaign entity
            // synchronize: true is convenient for development, BUT...
            // DO NOT USE synchronize: true IN PRODUCTION! Use migrations instead.
            synchronize: configService.get<string>('NODE_ENV') !== 'production',
            logging: false, // Enable for debugging DB queries
          }),
          inject: [ConfigService], // Inject ConfigService into the factory
        }),
        CampaignModule,
      ],
      controllers: [AppController],
      providers: [AppService],
    })
    export class AppModule {}
    • Add DATABASE_PATH=marketing_campaigns.sqlite to your .env (or choose another path/DB).
    • synchronize: true: Disable this in production and use TypeORM Migrations for controlled schema changes.
  4. Inject Repository and Update Service (src/campaign/campaign.module.ts & src/campaign/campaign.service.ts):

    • Module:

      typescript
      // src/campaign/campaign.module.ts
      import { Module } from '@nestjs/common';
      import { TypeOrmModule } from '@nestjs/typeorm'; // Import
      import { CampaignController } from './campaign.controller';
      import { CampaignService } from './campaign.service';
      import { Campaign } from './entities/campaign.entity'; // Import
      
      @Module({
        imports: [
          TypeOrmModule.forFeature([Campaign]), // Register Campaign entity repository
        ],
        controllers: [CampaignController],
        providers: [CampaignService],
      })
      export class CampaignModule {}
    • Service:

      typescript
      // src/campaign/campaign.service.ts
      import { Injectable, Logger } from '@nestjs/common';
      import { ConfigService } from '@nestjs/config';
      import { SNSClient, PublishCommand, PublishCommandInput } from '@aws-sdk/client-sns';
      import { InjectRepository } from '@nestjs/typeorm'; // Import
      import { Repository } from 'typeorm'; // Import
      import { Campaign, CampaignStatus } from './entities/campaign.entity'; // Import
      
      @Injectable()
      export class CampaignService {
        private readonly logger = new Logger(CampaignService.name);
        private readonly snsClient: SNSClient;
        private readonly topicArn: string;
      
        constructor(
          private configService: ConfigService,
          @InjectRepository(Campaign) // Inject Campaign repository
          private campaignRepository: Repository<Campaign>,
        ) {
          this.snsClient = new SNSClient({
            region: this.configService.getOrThrow<string>('AWS_REGION'),
            // Credentials sourced automatically
          });
          this.topicArn = this.configService.getOrThrow<string>('SNS_MARKETING_TOPIC_ARN');
          this.logger.log(`SNS Publisher initialized for topic: ${this.topicArn}`);
        }
      
        /**
         * Creates a campaign record, publishes to SNS, and updates the record status.
         * @returns An object containing the internal campaignId and the SNS messageId.
         */
        async publishCampaign(subject: string, messageBody: string): Promise<{ campaignId: string; messageId?: string }> {
          // 1. Create Campaign record in DB (status: PENDING)
          let campaign = this.campaignRepository.create({
            subject,
            messageBody,
            status: CampaignStatus.PENDING,
          });
          // Save initial record to get the ID
          campaign = await this.campaignRepository.save(campaign);
          this.logger.log(`Created campaign record ${campaign.id}`);
      
          const params: PublishCommandInput = {
            TopicArn: this.topicArn,
            Subject: subject,
            Message: messageBody,
          };
          this.logger.log(`Publishing campaign ""${subject}"" (ID: ${campaign.id}) to ${this.topicArn}`);
      
          try {
            const command = new PublishCommand(params);
            const response = await this.snsClient.send(command);
      
            // 2. Update Campaign record (status: SUBMITTED, snsMessageId)
            campaign.status = CampaignStatus.SUBMITTED;
            campaign.snsMessageId = response.MessageId;
            await this.campaignRepository.save(campaign); // Update the existing record
      
            this.logger.log(`Message published successfully: ${response.MessageId} for campaign ${campaign.id}`);
            return { campaignId: campaign.id, messageId: response.MessageId };
      
          } catch (error) {
            const errorMessage = error instanceof Error ? error.message : 'Unknown error';
            this.logger.error(`Failed to publish message to SNS for campaign ${campaign.id}: ${errorMessage}`, error instanceof Error ? error.stack : undefined);
      
            // 3. Update Campaign record (status: FAILED, failureReason)
            campaign.status = CampaignStatus.FAILED;
            campaign.failureReason = errorMessage.substring(0, 500); // Truncate if needed for DB column size
            await this.campaignRepository.save(campaign); // Update the existing record
      
            // Re-throw for the controller to handle
            throw new Error(`Failed to publish campaign ${campaign.id}: ${errorMessage}`);
          }
        }
        // Potential future methods:
        // async getCampaignStatus(id: string): Promise<CampaignStatus | null> { ... }
        // async listCampaigns(options: ListOptions): Promise<Campaign[]> { ... }
      }
    • Controller Update: If using the database approach, the service now returns { campaignId: string; messageId?: string }. You would update the controller's success response accordingly (as commented out in the controller example).

Frequently Asked Questions

How to send marketing messages with NestJS and AWS SNS?

You can send marketing messages by creating a NestJS application that interacts with the AWS SNS service. The application will have an API endpoint to receive campaign details, and it will publish those details as messages to an AWS SNS topic. Subscribers to the topic will then receive these messages.

What is AWS SNS used for in marketing campaigns?

AWS SNS (Simple Notification Service) is a pub/sub messaging service. It decouples the application sending messages from the systems that deliver them, allowing for high throughput and fault tolerance in marketing campaigns. Subscribers can include email addresses, SMS numbers, SQS queues, and other services.

Why use NestJS for building marketing campaign applications?

NestJS is a progressive Node.js framework known for building efficient and reliable server-side applications. Its modular architecture and dependency injection features make it well-suited for complex projects like managing marketing campaigns.

How to set up an AWS SNS topic for marketing messages?

First, log in to the AWS Management Console and navigate to the Simple Notification Service (SNS). Create a new Standard SNS topic with a descriptive name like "marketing-campaigns". After creating the topic, copy its ARN (Amazon Resource Name), as you'll need this to configure your NestJS application.

What AWS credentials are needed for NestJS to publish to SNS?

Your NestJS application will need AWS credentials with the "sns:Publish" permission for the specific SNS topic you created. You can achieve this by creating an IAM user with programmatic access and attaching the required policy, or, preferably, use an IAM role attached to the compute resource for production environments.

What is the role of a DTO in NestJS for sending campaigns?

DTOs (Data Transfer Objects) help validate incoming request data to your NestJS API. They define the structure of the request body, and combined with the ValidationPipe and class-validator, ensure data integrity by applying validation rules like @IsString, @IsNotEmpty, and @MaxLength.

How to handle errors when publishing to AWS SNS from NestJS?

Implement comprehensive error handling using NestJS exception filters. Catch SNS publishing errors, log them with details, and return appropriate HTTP error codes. Consider retry mechanisms for transient errors using the AWS SDK v3's built-in retry logic or application-level retry libraries like async-retry for critical campaigns.

When should I use a database for managing marketing campaigns in NestJS?

While not essential for simply publishing messages, a database is highly recommended for storing campaign metadata, tracking delivery status, and managing historical data. You can use TypeORM with an appropriate database like SQLite or PostgreSQL.

How to integrate TypeORM with NestJS for campaign management?

Install the necessary packages (@nestjs/typeorm, typeorm, and the database driver). Configure TypeORM in your AppModule, create entities to represent your campaign data, and inject the entity repository into your service to interact with the database.

Can I use environment variables for AWS credentials in NestJS?

Yes, you can use environment variables. However, for enhanced security in production, especially within AWS environments (like EC2 or Lambda), using IAM roles is the preferred and most secure approach, as IAM roles provide temporary credentials that are automatically rotated.

What are the prerequisites for building this NestJS and AWS SNS application?

You will need Node.js, npm or yarn, an AWS account with SNS permissions, the AWS CLI (for setup), and a basic understanding of NestJS and terminal operations. Familiarity with TypeScript is also beneficial.

What is the purpose of the .env file?

The .env file stores environment variables such as AWS credentials, region, and the SNS topic ARN. It's crucial to never commit this file to version control, as it contains sensitive information.

How to structure a NestJS project for AWS SNS integration?

Create a dedicated module (e.g., CampaignModule) containing a service (e.g., CampaignService) to handle the interaction with AWS SNS, a controller (e.g., CampaignController) to define API endpoints, and DTOs to validate requests.

What is the final outcome of building this NestJS AWS SNS project?

You'll have a NestJS application with a REST API endpoint to receive marketing campaign requests and publish them to an AWS SNS topic for distribution to subscribers like email, SMS, SQS queues, or other microservices.