npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@hapo-congbv/zalo-zns-nestjs

v1.0.5

Published

Zalo Notification Service (ZNS) NestJS module for sending notifications via Zalo ZNS API

Downloads

552

Readme

haposoft zalo-zns-nestjs

Zalo Notification Service (ZNS) NestJS module for sending notifications via Zalo ZNS API.

Installation

npm install @hapo-congbv/zalo-zns-nestjs

or

yarn add @hapo-congbv/zalo-zns-nestjs

Quick Start

1. Import Module

OAuth Configuration (Recommended)

The recommended approach is to use OAuth for automatic token management. This eliminates the need to manually manage access tokens.

First, create token storage services. Here's an example using Prisma:

Token Storage Service:

import { Injectable } from '@nestjs/common';
import { TokenStorage, ZaloTokenData } from '@hapo-congbv/zalo-zns-nestjs';
import { PrismaService } from './prisma.service';

@Injectable()
export class PrismaTokenStorageService implements TokenStorage {
  constructor(private readonly prisma: PrismaService) {}

  async getToken(): Promise<ZaloTokenData | null> {
    const token = await this.prisma.zaloToken.findUnique({
      where: { id: 'zalo-oauth-token' },
    });
    if (!token) return null;
    return {
      accessToken: token.accessToken,
      refreshToken: token.refreshToken,
      expiresAt: Number(token.expiresAt),
      refreshExpiresAt: token.refreshExpiresAt ? Number(token.refreshExpiresAt) : undefined,
    };
  }

  async saveToken(token: ZaloTokenData): Promise<void> {
    await this.prisma.zaloToken.upsert({
      where: { id: 'zalo-oauth-token' },
      create: {
        id: 'zalo-oauth-token',
        accessToken: token.accessToken,
        refreshToken: token.refreshToken,
        expiresAt: BigInt(token.expiresAt),
        refreshExpiresAt: token.refreshExpiresAt ? BigInt(token.refreshExpiresAt) : null,
      },
      update: {
        accessToken: token.accessToken,
        refreshToken: token.refreshToken,
        expiresAt: BigInt(token.expiresAt),
        refreshExpiresAt: token.refreshExpiresAt ? BigInt(token.refreshExpiresAt) : null,
      },
    });
  }

  async clearToken(): Promise<void> {
    await this.prisma.zaloToken
      .delete({
        where: { id: 'zalo-oauth-token' },
      })
      .catch(() => {});
  }
}

OAuth State Storage Service:

import { Injectable } from '@nestjs/common';
import { OAuthStateStorage } from '@hapo-congbv/zalo-zns-nestjs';
import { PrismaService } from './prisma.service';

@Injectable()
export class ZaloOAuthStateService implements OAuthStateStorage {
  constructor(private readonly prisma: PrismaService) {}

  async storeState(state: string, codeVerifier: string): Promise<void> {
    const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
    await this.prisma.zaloOAuthState.upsert({
      where: { state },
      create: { state, codeVerifier, expiresAt },
      update: { codeVerifier, expiresAt },
    });
  }

  async getCodeVerifier(state: string): Promise<string | null> {
    const record = await this.prisma.zaloOAuthState.findUnique({
      where: { state },
    });
    if (!record || new Date(record.expiresAt) < new Date()) {
      await this.deleteState(state);
      return null;
    }
    return record.codeVerifier;
  }

  async deleteState(state: string): Promise<void> {
    await this.prisma.zaloOAuthState
      .delete({
        where: { state },
      })
      .catch(() => {});
  }
}

Module Configuration:

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ZnsModule } from '@hapo-congbv/zalo-zns-nestjs';
import { PrismaModule } from './prisma/prisma.module';
import { PrismaTokenStorageService } from './config/zalo-token-storage.service';
import { ZaloOAuthStateService } from './config/zalo-oauth-state.service';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
    }),
    PrismaModule, // Import PrismaModule first to ensure services are available
    ZnsModule.forRootAsyncGlobal({
      imports: [ConfigModule, PrismaModule],
      useFactory: (
        configService: ConfigService,
        tokenStorage: PrismaTokenStorageService,
        oauthStateStorage: ZaloOAuthStateService,
      ) => ({
        oauthOptions: {
          appId: configService.get<string>('ZALO_APP_ID'),
          appSecret: configService.get<string>('ZALO_APP_SECRET'),
          oaId: configService.get<string>('ZALO_OA_ID'), // Official Account ID
          redirectUri: configService.get<string>('ZALO_REDIRECT_URI'),
        },
        tokenStorage,
        oauthStateStorage,
        enableOAuthController: true, // Enables /zalo/oauth/* endpoints
        apiUrl: configService.get<string>('ZALO_API_URL', 'https://business.openapi.zalo.me'),
        timeout: configService.get<number>('ZALO_TIMEOUT', 30000),
      }),
      inject: [ConfigService, PrismaTokenStorageService, ZaloOAuthStateService],
    }),
  ],
})
export class AppModule {}

Note: If you don't provide custom tokenStorage and oauthStateStorage, the package will use in-memory storage (tokens will be lost on server restart).

Global Module

If you want to use ZnsService globally without importing ZnsModule in every module:

ZnsModule.forRootAsyncGlobal({
  imports: [ConfigModule, PrismaModule],
  useFactory: (
    configService: ConfigService,
    tokenStorage: PrismaTokenStorageService,
    oauthStateStorage: ZaloOAuthStateService,
  ) => ({
    oauthOptions: {
      appId: configService.get<string>('ZALO_APP_ID'),
      appSecret: configService.get<string>('ZALO_APP_SECRET'),
      oaId: configService.get<string>('ZALO_OA_ID'),
      redirectUri: configService.get<string>('ZALO_REDIRECT_URI'),
    },
    tokenStorage,
    oauthStateStorage,
    enableOAuthController: true,
  }),
  inject: [ConfigService, PrismaTokenStorageService, ZaloOAuthStateService],
});

2. Use ZnsService

import { Injectable } from '@nestjs/common';
import { ZnsMessage, ZnsService } from '@hapo-congbv/zalo-zns-nestjs';

@Injectable()
export class NotificationService {
  constructor(private readonly znsService: ZnsService) {}

  async sendNotification() {
    const message: ZnsMessage = {
      phone: '0912345678',
      templateId: 'your-template-id',
      templateData: {
        name: 'John Doe',
        code: '123456',
      },
      trackingId: 'optional-tracking-id',
    };

    const result = await this.znsService.sendMessage(message);

    if (result.error === 0) {
      console.log('Message sent successfully!', result.data?.trackingId);
    } else {
      console.error('Failed to send message:', result.message);
    }
  }

  async sendBulkNotifications() {
    const messages: ZnsMessage[] = [
      {
        phone: '0912345678',
        templateId: 'template-1',
        templateData: { name: 'User 1' },
      },
      {
        phone: '0987654321',
        templateId: 'template-2',
        templateData: { name: 'User 2' },
      },
    ];

    const results = await this.znsService.sendBulkMessages(messages);
    console.log('Bulk send results:', results);
  }
}

API Reference

ZnsModule

forRoot(options: ZnsModuleOptions)

Register ZNS module with synchronous options.

forRootAsync(options: ZnsAsyncOptions)

Register ZNS module with asynchronous options.

forRootGlobal(options: ZnsModuleOptions)

Register ZNS module as global with synchronous options.

forRootAsyncGlobal(options: ZnsAsyncOptions)

Register ZNS module as global with asynchronous options.

ZnsService

sendMessage(message: ZnsMessage): Promise<ZnsSendResponse>

Send a single ZNS notification.

sendBulkMessages(messages: ZnsMessage[]): Promise<ZnsSendResponse[]>

Send multiple ZNS notifications.

Interfaces

ZnsModuleOptions

interface ZnsModuleOptions {
  // Option 1: OAuth configuration (recommended)
  oauthOptions?: {
    appId: string;
    appSecret: string;
    oaId: string; // Official Account ID
    redirectUri: string;
  };
  tokenStorage?: TokenStorage; // Optional: Custom token storage (defaults to in-memory)
  oauthStateStorage?: OAuthStateStorage; // Optional: Custom OAuth state storage (defaults to in-memory)
  enableOAuthController?: boolean; // Optional: Enable OAuth endpoints (default: true if oauthOptions provided)

  // Option 2: Direct access token (legacy mode)
  accessToken?: string;

  // Common options
  apiUrl?: string; // Optional: API URL (default: 'https://business.openapi.zalo.me')
  timeout?: number; // Optional: Request timeout in ms (default: 30000)
}

Note: Either provide oauthOptions (recommended) or accessToken (legacy). OAuth mode requires tokenStorage and oauthStateStorage for production use.

ZnsMessage

interface ZnsMessage {
  phone: string; // Required: Phone number
  templateId: string; // Required: ZNS template ID
  templateData?: Record<string, any>; // Optional: Template data
  trackingId?: string; // Optional: Tracking ID
}

ZnsSendResponse

interface ZnsSendResponse {
  error: number; // 0 = success, non-zero = error
  message: string; // Response message
  data?: {
    trackingId: string; // Tracking ID if successful
  };
}

OAuth Endpoints

When enableOAuthController is true, the following endpoints are available:

  • GET /zalo/oauth/authorize - Get authorization URL
  • GET /zalo/oauth/callback - Handle OAuth callback
  • GET /zalo/oauth/token-status - Check authorization status
  • POST /zalo/oauth/refresh - Manually refresh token
  • DELETE /zalo/oauth/token - Clear/revoke stored token (useful when changing app configuration)

OAuth Flow

  1. Call GET /zalo/oauth/authorize to get the authorization URL
  2. Redirect user to the authorization URL
  3. User authorizes the application on Zalo
  4. Zalo redirects to your callback URL with authorization code
  5. The callback endpoint automatically exchanges the code for tokens
  6. Tokens are stored and automatically refreshed when needed

The package automatically refreshes tokens when they expire, so you don't need to manually manage token lifecycle.

Legacy Mode (Static Access Token)

If you already have a Zalo access token and prefer to use it directly without OAuth flow, you can configure it as follows:

Synchronous Configuration

import { Module } from '@nestjs/common';
import { ZnsModule } from '@hapo-congbv/zalo-zns-nestjs';

@Module({
  imports: [
    ZnsModule.forRootGlobal({
      accessToken: 'your-zalo-access-token',
      apiUrl: 'https://business.openapi.zalo.me', // Optional
      timeout: 30000, // Optional, default 30000ms
    }),
  ],
})
export class AppModule {}

Asynchronous Configuration (Recommended for Legacy Mode)

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ZnsModule } from '@hapo-congbv/zalo-zns-nestjs';

@Module({
  imports: [
    ConfigModule.forRoot(),
    ZnsModule.forRootAsyncGlobal({
      imports: [ConfigModule],
      useFactory: (configService: ConfigService) => ({
        accessToken: configService.get<string>('ZALO_ACCESS_TOKEN'),
        apiUrl: configService.get<string>('ZALO_API_URL', 'https://business.openapi.zalo.me'),
        timeout: configService.get<number>('ZALO_TIMEOUT', 30000),
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}

How to Get Access Token

  1. Go to Zalo Developer Console
  2. Navigate to your app settings
  3. Generate or copy your access token
  4. Add it to your .env file as ZALO_ACCESS_TOKEN

Important Notes:

  • ⚠️ Token Expiration: Static access tokens expire after 25 hours and require manual refresh
  • ⚠️ Manual Management: You must manually obtain a new token from Zalo Developer Console when the token expires
  • OAuth Recommended: OAuth mode is strongly recommended for production use as it handles token refresh automatically
  • 💡 Use Cases: Static token mode is suitable for:
    • Development and testing
    • Quick prototyping
    • When you have a specific reason not to use OAuth

Environment Variables

OAuth Mode (Recommended)

ZALO_APP_ID=your-app-id
ZALO_APP_SECRET=your-app-secret
ZALO_OA_ID=your-official-account-id
ZALO_REDIRECT_URI=https://your-domain.com/zalo/oauth/callback
ZALO_API_URL=https://business.openapi.zalo.me
ZALO_TIMEOUT=30000

Required for OAuth:

  • ZALO_APP_ID - Your Zalo App ID
  • ZALO_APP_SECRET - Your Zalo App Secret
  • ZALO_OA_ID - Your Official Account ID
  • ZALO_REDIRECT_URI - OAuth callback URL (must match Zalo app configuration)

Optional:

  • ZALO_API_URL - API base URL (default: https://business.openapi.zalo.me)
  • ZALO_TIMEOUT - Request timeout in milliseconds (default: 30000)

Legacy Mode (Static Token)

If you already have a Zalo access token and prefer to use it directly without OAuth flow:

ZALO_ACCESS_TOKEN=your-access-token
ZALO_API_URL=https://business.openapi.zalo.me
ZALO_TIMEOUT=30000

Required for Legacy Mode:

  • ZALO_ACCESS_TOKEN - Your Zalo access token (obtained from Zalo Developer Console)

Optional:

  • ZALO_API_URL - API base URL (default: https://business.openapi.zalo.me)
  • ZALO_TIMEOUT - Request timeout in milliseconds (default: 30000)

Important Notes:

  • Static access tokens expire after 25 hours and require manual refresh
  • You need to manually obtain a new token from Zalo Developer Console when the token expires
  • OAuth mode is strongly recommended for production use as it handles token refresh automatically
  • Use static token mode only for development, testing, or when you have a specific reason not to use OAuth

License

MIT

Support

For issues and feature requests, please visit GitHub Issues.