@hapo-congbv/zalo-zns-nestjs
v1.0.5
Published
Zalo Notification Service (ZNS) NestJS module for sending notifications via Zalo ZNS API
Downloads
552
Maintainers
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-nestjsor
yarn add @hapo-congbv/zalo-zns-nestjsQuick 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 URLGET /zalo/oauth/callback- Handle OAuth callbackGET /zalo/oauth/token-status- Check authorization statusPOST /zalo/oauth/refresh- Manually refresh tokenDELETE /zalo/oauth/token- Clear/revoke stored token (useful when changing app configuration)
OAuth Flow
- Call
GET /zalo/oauth/authorizeto get the authorization URL - Redirect user to the authorization URL
- User authorizes the application on Zalo
- Zalo redirects to your callback URL with authorization code
- The callback endpoint automatically exchanges the code for tokens
- 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
- Go to Zalo Developer Console
- Navigate to your app settings
- Generate or copy your access token
- Add it to your
.envfile asZALO_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=30000Required for OAuth:
ZALO_APP_ID- Your Zalo App IDZALO_APP_SECRET- Your Zalo App SecretZALO_OA_ID- Your Official Account IDZALO_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=30000Required 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.
