| name | sms-development |
| description | Development guide for @rytass/sms base package (簡訊基底套件開發指南). Use when creating new SMS adapters (新增簡訊 adapter), understanding base interfaces, or extending SMS functionality. Covers SMSService interface and implementation patterns for Taiwan SMS providers (台灣簡訊服務提供商). Keywords - SMS adapter, 簡訊 adapter, service interface, 服務介面, message delivery, 訊息發送, batch processing, 批次處理, number normalization, 號碼正規化, Every8D, 互動資通, Interactive Communications |
SMS Development Guide
This skill provides guidance for developers working with the @rytass/sms base package, including creating new SMS adapters for Taiwan SMS service providers.
Overview
The @rytass/sms package defines the core interfaces and types that all SMS adapters must implement. It follows the adapter pattern to provide a unified API across different SMS providers.
Architecture
@rytass/sms (Base Package)
│
├── SMSService<Request, SendResponse, MultiTarget> # Service interface
├── SMSRequest # Single message interface
├── SMSSendResponse # Response interface
├── MultiTargetRequest # Batch message interface
├── SMSRequestResult # Status enum
└── Taiwan Phone Number Helpers # Normalization utilities
@rytass/sms-adapter-* # Provider implementations
│
├── [Provider]SMSService # Implements SMSService
├── [Provider]SMSRequest # Extends SMSRequest
├── [Provider]SMSSendResponse # Extends SMSSendResponse
└── [Provider] specific types and errors
Installation
npm install @rytass/sms
Core Interfaces
SMSService
The main interface that all SMS adapters must implement:
interface SMSService<
Request extends SMSRequest,
SendResponse extends SMSSendResponse,
MultiTarget extends MultiTargetRequest,
> {
send(request: Request[]): Promise<SendResponse[]>;
send(request: Request): Promise<SendResponse>;
send(request: MultiTarget): Promise<SendResponse[]>;
}
Base Request Types
interface SMSRequest {
mobile: string;
text: string;
}
interface MultiTargetRequest {
mobileList: string[];
text: string;
}
Base Response Types
enum SMSRequestResult {
SUCCESS = 'SUCCESS',
FAILED = 'FAILED',
}
interface SMSSendResponse {
messageId?: string;
status: SMSRequestResult;
mobile: string;
}
Taiwan Phone Number Utilities
The base package provides utilities for handling Taiwan mobile numbers:
const TAIWAN_PHONE_NUMBER_RE = /^(0|\+?886-?)9\d{2}-?\d{3}-?\d{3}$/;
function normalizedTaiwanMobilePhoneNumber(mobile: string): string;
Implementation Guide
Step 1: Define Provider-Specific Types
Create interfaces extending the base types:
import {
SMSRequest,
SMSSendResponse,
MultiTargetRequest,
SMSRequestResult,
} from '@rytass/sms';
export interface MyProviderSMSRequestInit {
username: string;
password: string;
baseUrl?: string;
onlyTaiwanMobileNumber?: boolean;
}
export enum MyProviderError {
INVALID_CREDENTIALS = -1,
FORMAT_ERROR = -2,
INSUFFICIENT_BALANCE = -3,
RATE_LIMIT_EXCEEDED = -4,
UNKNOWN = -99,
}
export interface MyProviderSMSRequest extends SMSRequest {
mobile: string;
text: string;
}
export interface MyProviderSMSSendResponse extends SMSSendResponse {
messageId?: string;
status: SMSRequestResult;
mobile: string;
errorMessage?: string;
errorCode?: MyProviderError;
}
export interface MyProviderSMSMultiTargetRequest extends MultiTargetRequest {
mobileList: string[];
text: string;
}
Step 2: Implement the SMS Service
import {
SMSService,
SMSRequestResult,
normalizedTaiwanMobilePhoneNumber,
TAIWAN_PHONE_NUMBER_RE,
} from '@rytass/sms';
import axios from 'axios';
export class SMSServiceMyProvider implements SMSService<
MyProviderSMSRequest,
MyProviderSMSSendResponse,
MyProviderSMSMultiTargetRequest
> {
private readonly username: string;
private readonly password: string;
private readonly baseUrl: string;
private readonly onlyTaiwanMobileNumber: boolean;
constructor(options: MyProviderSMSRequestInit) {
this.username = options.username;
this.password = options.password;
this.baseUrl = options.baseUrl || 'https://api.myprovider.com';
this.onlyTaiwanMobileNumber = options.onlyTaiwanMobileNumber || false;
}
async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;
async send(
requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
): Promise<MyProviderSMSSendResponse | MyProviderSMSSendResponse[]> {
if (
(Array.isArray(requests) && !requests.length) ||
((requests as MyProviderSMSMultiTargetRequest).mobileList &&
!(requests as MyProviderSMSMultiTargetRequest).mobileList?.length)
) {
throw new Error('No target provided.');
}
const processedRequests = this.processRequests(requests);
const results = await this.sendToProvider(processedRequests);
return this.formatResults(requests, results);
}
private processRequests(
requests: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
): Array<{ mobile: string; text: string }> {
const requestArray = Array.isArray(requests) ? requests : [requests];
const processed: Array<{ mobile: string; text: string }> = [];
for (const request of requestArray) {
if ((request as MyProviderSMSMultiTargetRequest).mobileList) {
const multiTarget = request as MyProviderSMSMultiTargetRequest;
for (const mobile of multiTarget.mobileList) {
const normalizedMobile = this.validateAndNormalizeMobile(mobile);
processed.push({
mobile: normalizedMobile,
text: multiTarget.text,
});
}
} else {
const singleRequest = request as MyProviderSMSRequest;
const normalizedMobile = this.validateAndNormalizeMobile(singleRequest.mobile);
processed.push({
mobile: normalizedMobile,
text: singleRequest.text,
});
}
}
return processed;
}
private validateAndNormalizeMobile(mobile: string): string {
if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
return normalizedTaiwanMobilePhoneNumber(mobile);
}
if (this.onlyTaiwanMobileNumber) {
throw new Error(
`${mobile} is not taiwan mobile phone (\`onlyTaiwanMobileNumber\` option is true)`
);
}
return mobile;
}
private async sendToProvider(
requests: Array<{ mobile: string; text: string }>,
): Promise<Map<string, MyProviderSMSSendResponse>> {
const batches = this.groupByMessage(requests);
const results = new Map<string, MyProviderSMSSendResponse>();
for (const [message, mobileList] of batches.entries()) {
try {
const response = await this.callProviderAPI(mobileList, message);
const batchResults = this.parseAPIResponse(response, mobileList, message);
for (const [key, result] of batchResults.entries()) {
results.set(key, result);
}
} catch (error) {
for (const mobile of mobileList) {
results.set(`${message}:${mobile}`, {
status: SMSRequestResult.FAILED,
mobile,
errorMessage: error.message,
errorCode: MyProviderError.UNKNOWN,
});
}
}
}
return results;
}
private groupByMessage(
requests: Array<{ mobile: string; text: string }>,
): Map<string, string[]> {
const batches = new Map<string, string[]>();
for (const request of requests) {
const existing = batches.get(request.text) || [];
batches.set(request.text, [...existing, request.mobile]);
}
return batches;
}
private async callProviderAPI(
mobileList: string[],
message: string,
): Promise<any> {
const { data } = await axios.post(
`${this.baseUrl}/API21/HTTP/SendSMS.ashx`,
new URLSearchParams({
UID: this.username,
PWD: this.password,
MSG: message,
DEST: mobileList.join(','),
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return data;
}
private parseAPIResponse(
response: any,
mobileList: string[],
message: string,
): Map<string, MyProviderSMSSendResponse> {
const results = new Map<string, MyProviderSMSSendResponse>();
if (response.success) {
for (const mobile of mobileList) {
results.set(`${message}:${mobile}`, {
messageId: response.messageId,
status: SMSRequestResult.SUCCESS,
mobile,
});
}
} else {
for (const mobile of mobileList) {
results.set(`${message}:${mobile}`, {
status: SMSRequestResult.FAILED,
mobile,
errorMessage: response.errorMessage,
errorCode: response.errorCode,
});
}
}
return results;
}
private formatResults(
originalRequest: MyProviderSMSMultiTargetRequest | MyProviderSMSRequest | MyProviderSMSRequest[],
results: Map<string, MyProviderSMSSendResponse>,
): MyProviderSMSSendResponse | MyProviderSMSSendResponse[] {
if ((originalRequest as MyProviderSMSMultiTargetRequest).mobileList) {
const multiTarget = originalRequest as MyProviderSMSMultiTargetRequest;
return multiTarget.mobileList.map(mobile => {
const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(mobile)
? normalizedTaiwanMobilePhoneNumber(mobile)
: mobile;
return results.get(`${multiTarget.text}:${normalizedMobile}`)!;
});
}
if (Array.isArray(originalRequest)) {
return originalRequest.map(request => {
const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(request.mobile)
? normalizedTaiwanMobilePhoneNumber(request.mobile)
: request.mobile;
return results.get(`${request.text}:${normalizedMobile}`)!;
});
}
const singleRequest = originalRequest as MyProviderSMSRequest;
const normalizedMobile = TAIWAN_PHONE_NUMBER_RE.test(singleRequest.mobile)
? normalizedTaiwanMobilePhoneNumber(singleRequest.mobile)
: singleRequest.mobile;
return results.get(`${singleRequest.text}:${normalizedMobile}`)!;
}
}
Step 3: Export Public API
export { SMSServiceMyProvider } from './sms-service-my-provider';
export * from './typings';
Step 4: Add Tests
import { SMSServiceMyProvider } from '../src/sms-service-my-provider';
import { SMSRequestResult } from '@rytass/sms';
describe('SMSServiceMyProvider', () => {
let smsService: SMSServiceMyProvider;
beforeEach(() => {
smsService = new SMSServiceMyProvider({
username: 'test-username',
password: 'test-password',
onlyTaiwanMobileNumber: true,
});
});
describe('send - single SMS', () => {
it('should send single SMS successfully', async () => {
const result = await smsService.send({
mobile: '0987654321',
text: 'Test message',
});
expect(result.status).toBe(SMSRequestResult.SUCCESS);
expect(result.mobile).toBe('0987654321');
expect(result.messageId).toBeDefined();
});
it('should normalize Taiwan phone number', async () => {
const result = await smsService.send({
mobile: '+886987654321',
text: 'Test message',
});
expect(result.mobile).toBe('0987654321');
});
it('should reject non-Taiwan number when onlyTaiwanMobileNumber is true', async () => {
await expect(
smsService.send({
mobile: '+1234567890',
text: 'Test message',
})
).rejects.toThrow('is not taiwan mobile phone');
});
});
describe('send - batch SMS', () => {
it('should send same message to multiple recipients', async () => {
const results = await smsService.send({
mobileList: ['0987654321', '0912345678', '0923456789'],
text: 'Batch message',
});
expect(results).toHaveLength(3);
expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
});
it('should send different messages to multiple recipients', async () => {
const results = await smsService.send([
{ mobile: '0987654321', text: 'Message 1' },
{ mobile: '0912345678', text: 'Message 2' },
{ mobile: '0923456789', text: 'Message 3' },
]);
expect(results).toHaveLength(3);
expect(results.every(r => r.status === SMSRequestResult.SUCCESS)).toBe(true);
});
});
describe('error handling', () => {
it('should handle API errors gracefully', async () => {
});
it('should return FAILED status on delivery failure', async () => {
});
});
});
Implementation Checklist
When implementing a new SMS adapter, ensure:
Required Features
Recommended Features
Quality Assurance
Best Practices
1. Phone Number Handling
import { normalizedTaiwanMobilePhoneNumber, TAIWAN_PHONE_NUMBER_RE } from '@rytass/sms';
private validateMobile(mobile: string): string {
if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
return normalizedTaiwanMobilePhoneNumber(mobile);
}
if (this.onlyTaiwanMobileNumber) {
throw new Error(`Invalid Taiwan mobile number: ${mobile}`);
}
return mobile;
}
private validateMobile(mobile: string): string {
if (!/^09\d{8}$/.test(mobile)) {
throw new Error('Invalid number');
}
return mobile;
}
2. Error Handling
catch (error) {
return {
status: SMSRequestResult.FAILED,
mobile,
errorMessage: error.response?.data?.message || error.message,
errorCode: this.mapProviderErrorCode(error.response?.data?.code),
};
}
catch (error) {
return {
status: SMSRequestResult.FAILED,
mobile,
};
}
3. Batch Optimization
private groupByMessage(requests: Array<{ mobile: string; text: string }>) {
const batches = new Map<string, string[]>();
for (const request of requests) {
const existing = batches.get(request.text) || [];
batches.set(request.text, [...existing, request.mobile]);
}
return batches;
}
for (const request of requests) {
await this.callAPI(request.mobile, request.text);
}
4. Type Safety
export class SMSServiceMyProvider implements SMSService<
MyProviderSMSRequest,
MyProviderSMSSendResponse,
MyProviderSMSMultiTargetRequest
> {
}
export class SMSServiceMyProvider {
async send(request: any): Promise<any> {
}
}
5. Configuration
export class SMSServiceMyProvider {
constructor(options: MyProviderSMSRequestInit) {
this.baseUrl = options.baseUrl || this.getDefaultBaseUrl();
}
private getDefaultBaseUrl(): string {
return process.env.NODE_ENV === 'production'
? 'https://api.myprovider.com'
: 'https://api-staging.myprovider.com';
}
}
export class SMSServiceMyProvider {
private baseUrl = 'https://api.myprovider.com';
}
API Reference
Base Package Exports
export {
SMSService,
SMSRequest,
SMSSendResponse,
MultiTargetRequest,
SMSRequestResult,
};
export {
TAIWAN_PHONE_NUMBER_RE,
normalizedTaiwanMobilePhoneNumber,
};
Taiwan Number Validation
Supported Formats:
09XXXXXXXX - Standard Taiwan format
0912-345-678 - With dashes
0912 345 678 - With spaces
+8869XXXXXXXX - International with +
8869XXXXXXXX - International without +
Normalization Output:
- Always returns
09XXXXXXXX format
- Removes dashes, spaces, and country code
- Converts
+886 or 886 prefix to 0
Examples
Minimal Adapter Implementation
import {
SMSService,
SMSRequest,
SMSSendResponse,
MultiTargetRequest,
SMSRequestResult,
} from '@rytass/sms';
interface SimpleSMSRequest extends SMSRequest {
mobile: string;
text: string;
}
interface SimpleSMSResponse extends SMSSendResponse {
messageId?: string;
status: SMSRequestResult;
mobile: string;
}
interface SimpleMultiTargetRequest extends MultiTargetRequest {
mobileList: string[];
text: string;
}
export class SimpleSMSService implements SMSService<
SimpleSMSRequest,
SimpleSMSResponse,
SimpleMultiTargetRequest
> {
async send(requests: SimpleSMSRequest[]): Promise<SimpleSMSResponse[]>;
async send(request: SimpleSMSRequest): Promise<SimpleSMSResponse>;
async send(request: SimpleMultiTargetRequest): Promise<SimpleSMSResponse[]>;
async send(request: any): Promise<any> {
if (Array.isArray(request)) {
return Promise.all(request.map(r => this.sendSingle(r)));
}
if (request.mobileList) {
return Promise.all(
request.mobileList.map(mobile =>
this.sendSingle({ mobile, text: request.text })
)
);
}
return this.sendSingle(request);
}
private async sendSingle(request: SimpleSMSRequest): Promise<SimpleSMSResponse> {
return {
messageId: 'MSG-' + Date.now(),
status: SMSRequestResult.SUCCESS,
mobile: request.mobile,
};
}
}
Common Provider Patterns
HTTP-based API
Most Taiwan SMS providers use HTTP POST:
private async callProviderAPI(mobile: string, text: string): Promise<any> {
const { data } = await axios.post(
`${this.baseUrl}/api/sms/send`,
new URLSearchParams({
username: this.username,
password: this.password,
mobile: mobile,
message: text,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
return data;
}
CSV Response Format
Some providers return CSV responses:
private parseCSVResponse(data: string): {
messageId: string;
status: SMSRequestResult;
} {
const [credit, sent, cost, unsent, messageId] = data.split(',');
return {
messageId,
status: messageId ? SMSRequestResult.SUCCESS : SMSRequestResult.FAILED,
};
}
Signature-based Authentication
Some providers require request signatures:
import crypto from 'crypto';
private generateSignature(params: Record<string, string>): string {
const sorted = Object.keys(params)
.sort()
.map(key => `${key}=${params[key]}`)
.join('&');
return crypto
.createHash('md5')
.update(sorted + this.secretKey)
.digest('hex');
}
Troubleshooting
Common Issues
TypeScript Errors:
async send(requests: MyProviderSMSRequest[]): Promise<MyProviderSMSSendResponse[]>;
async send(request: MyProviderSMSRequest): Promise<MyProviderSMSSendResponse>;
async send(request: MyProviderSMSMultiTargetRequest): Promise<MyProviderSMSSendResponse[]>;
Phone Number Validation:
if (TAIWAN_PHONE_NUMBER_RE.test(mobile)) {
mobile = normalizedTaiwanMobilePhoneNumber(mobile);
}
Batch Optimization:
const batches = requests.reduce((map, req) => {
const list = map.get(req.text) || [];
map.set(req.text, [...list, req.mobile]);
return map;
}, new Map<string, string[]>());
Publishing Checklist
Before publishing your adapter:
Resources
For reference implementations:
For usage guidance: