Publishing from JavaScript¶
Send notifications to Notifer topics using JavaScript (browser) and Node.js with fetch or the official SDK (coming soon).
Quick Start¶
Browser (fetch)¶
fetch('https://app.notifer.io/my-topic', {
method: 'POST',
headers: {
'Content-Type': 'text/plain'
},
body: 'Your message here'
})
.then(res => res.json())
.then(data => console.log(data));
Node.js (fetch)¶
// Node.js 18+ has built-in fetch
const response = await fetch('https://app.notifer.io/my-topic', {
method: 'POST',
body: 'Your message here'
});
const data = await response.json();
console.log(data);
Output:
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"topic": "my-topic",
"message": "Your message here",
"priority": 3,
"created_at": "2025-11-22T10:30:00Z"
}
Installation¶
Browser¶
No installation needed - fetch is built into modern browsers.
Node.js¶
Node.js 18+: Built-in fetch API
Node.js < 18: Install node-fetch
Official SDK (Coming Soon)¶
The official SDK will provide TypeScript types, retries, and better error handling.
Basic Publishing¶
Simple Message¶
async function sendNotification(topic, message) {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// Usage
const result = await sendNotification('server-alerts', 'Server is down!');
console.log(`Message ID: ${result.id}`);
With Title and Priority¶
async function sendAlert(topic, title, message, priority = 3) {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: {
'X-Title': title,
'X-Priority': priority.toString()
},
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// Usage
await sendAlert(
'production-alerts',
'Database Error',
'Connection timeout on prod-db-01',
5
);
With Tags¶
async function sendTaggedMessage(topic, message, tags, priority = 3) {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: {
'X-Priority': priority.toString(),
'X-Tags': tags.join(',')
},
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// Usage
await sendTaggedMessage(
'monitoring',
'CPU usage at 95%',
['warning', 'cpu', 'prod-web-01'],
4
);
Advanced Features¶
Markdown Formatting¶
async function sendMarkdownMessage(topic, message, title = null, priority = 3) {
const headers = {
'X-Markdown': 'true',
'X-Priority': priority.toString()
};
if (title) {
headers['X-Title'] = title;
}
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers,
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
// Usage
const markdownMessage = `
## Deployment Summary
**Status:** ✅ Success
**Version:** v2.1.0
**Duration:** 3m 45s
### Changes
- Added user authentication
- Fixed payment bug
- Updated dependencies
[View Release Notes](https://github.com/example/repo/releases/v2.1.0)
`;
await sendMarkdownMessage(
'deployments',
markdownMessage,
'Deploy v2.1.0',
3
);
Private Topics with Authentication¶
class NotiferClient {
constructor(apiKey = null, jwtToken = null) {
this.baseUrl = 'https://app.notifer.io';
this.headers = {};
if (apiKey) {
this.headers['Authorization'] = `Bearer ${apiKey}`;
} else if (jwtToken) {
this.headers['Authorization'] = `Bearer ${jwtToken}`;
}
}
async publish(topic, message, options = {}) {
const {
title = null,
priority = 3,
tags = null,
markdown = false
} = options;
const headers = { ...this.headers };
headers['X-Priority'] = priority.toString();
if (title) {
headers['X-Title'] = title;
}
if (tags) {
headers['X-Tags'] = tags.join(',');
}
if (markdown) {
headers['X-Markdown'] = 'true';
}
const response = await fetch(`${this.baseUrl}/${topic}`, {
method: 'POST',
headers,
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
}
// Usage
const client = new NotiferClient('your-api-key-here');
await client.publish(
'private-alerts',
'Database connection lost',
{
title: 'Critical Issue',
priority: 5,
tags: ['critical', 'database']
}
);
Real-World Examples¶
Frontend Error Tracking¶
class ErrorTracker {
constructor(topic = 'frontend-errors', apiKey = null) {
this.topic = topic;
this.baseUrl = 'https://app.notifer.io';
this.apiKey = apiKey;
}
async logError(error, context = {}) {
const errorType = error.name;
const errorMessage = error.message;
const stackTrace = error.stack || 'No stack trace available';
const message = `
## ${errorType}
**Error:** ${errorMessage}
**Page:** ${window.location.href}
**User Agent:** ${navigator.userAgent}
**Context:**
${JSON.stringify(context, null, 2)}
**Stack Trace:**
\`\`\`
${stackTrace}
\`\`\`
`;
try {
const headers = {
'X-Title': `Error: ${errorType}`,
'X-Priority': '5',
'X-Tags': `error,${errorType.toLowerCase()},frontend`,
'X-Markdown': 'true'
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
await fetch(`${this.baseUrl}/${this.topic}`, {
method: 'POST',
headers,
body: message
});
} catch (e) {
// Don't let notification failure break the app
console.error('Failed to log error to Notifer:', e);
}
}
}
// Setup global error handler
const tracker = new ErrorTracker('frontend-errors');
window.addEventListener('error', (event) => {
tracker.logError(event.error, {
filename: event.filename,
lineno: event.lineno,
colno: event.colno
});
});
window.addEventListener('unhandledrejection', (event) => {
tracker.logError(new Error(event.reason), {
type: 'unhandled-promise-rejection'
});
});
Node.js Server Monitoring¶
import os from 'os';
class ServerMonitor {
constructor(topic = 'server-monitoring') {
this.topic = topic;
this.cpuThreshold = 80;
this.memoryThreshold = 85;
}
getCPUUsage() {
const cpus = os.cpus();
let totalIdle = 0;
let totalTick = 0;
cpus.forEach(cpu => {
for (const type in cpu.times) {
totalTick += cpu.times[type];
}
totalIdle += cpu.times.idle;
});
return 100 - ~~(100 * totalIdle / totalTick);
}
getMemoryUsage() {
const totalMem = os.totalmem();
const freeMem = os.freemem();
return ((totalMem - freeMem) / totalMem) * 100;
}
async sendAlert(title, message, priority = 4) {
try {
await fetch(`https://app.notifer.io/${this.topic}`, {
method: 'POST',
headers: {
'X-Title': title,
'X-Priority': priority.toString(),
'X-Tags': 'monitoring,server'
},
body: message
});
} catch (error) {
console.error('Failed to send alert:', error);
}
}
async checkSystem() {
const cpuPercent = this.getCPUUsage();
const memoryPercent = this.getMemoryUsage().toFixed(2);
if (cpuPercent > this.cpuThreshold) {
await this.sendAlert(
`High CPU Usage: ${cpuPercent}%`,
`CPU usage is at ${cpuPercent}% (threshold: ${this.cpuThreshold}%)`,
4
);
}
if (memoryPercent > this.memoryThreshold) {
await this.sendAlert(
`High Memory Usage: ${memoryPercent}%`,
`Memory usage is at ${memoryPercent}% (threshold: ${this.memoryThreshold}%)`,
4
);
}
}
start(intervalMs = 60000) {
console.log('Starting server monitoring...');
this.checkSystem(); // Initial check
setInterval(() => this.checkSystem(), intervalMs);
}
}
// Usage
const monitor = new ServerMonitor();
monitor.start(); // Check every minute
Express.js API Integration¶
import express from 'express';
const app = express();
class NotiferLogger {
constructor(topic, apiKey) {
this.topic = topic;
this.apiKey = apiKey;
}
async log(level, message, metadata = {}) {
const priority = {
'debug': 1,
'info': 2,
'warn': 4,
'error': 5
}[level] || 3;
const formattedMessage = `
**Level:** ${level.toUpperCase()}
**Timestamp:** ${new Date().toISOString()}
${message}
**Metadata:**
\`\`\`json
${JSON.stringify(metadata, null, 2)}
\`\`\`
`;
try {
await fetch(`https://app.notifer.io/${this.topic}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'X-Title': `${level.toUpperCase()}: ${message.substring(0, 50)}`,
'X-Priority': priority.toString(),
'X-Tags': `log,${level}`,
'X-Markdown': 'true'
},
body: formattedMessage
});
} catch (error) {
console.error('Failed to send log to Notifer:', error);
}
}
}
const logger = new NotiferLogger('api-logs', process.env.NOTIFER_API_KEY);
// Error handling middleware
app.use((err, req, res, next) => {
logger.log('error', err.message, {
path: req.path,
method: req.method,
body: req.body,
stack: err.stack
});
res.status(500).json({ error: 'Internal server error' });
});
// API endpoint example
app.post('/api/payment', async (req, res) => {
try {
// Process payment...
await logger.log('info', 'Payment processed successfully', {
amount: req.body.amount,
userId: req.user.id
});
res.json({ success: true });
} catch (error) {
await logger.log('error', 'Payment processing failed', {
error: error.message,
userId: req.user.id
});
throw error;
}
});
app.listen(3000);
CI/CD GitHub Actions Integration¶
// .github/workflows/notify.yml
// Use this script in GitHub Actions
import fetch from 'node-fetch';
async function notifyDeployment(status, metadata) {
const {
branch = process.env.GITHUB_REF_NAME,
commit = process.env.GITHUB_SHA,
workflow = process.env.GITHUB_WORKFLOW,
actor = process.env.GITHUB_ACTOR
} = metadata;
const emoji = status === 'success' ? '✅' : '❌';
const priority = status === 'success' ? 3 : 5;
const message = `
## ${emoji} ${workflow} ${status === 'success' ? 'Succeeded' : 'Failed'}
**Branch:** \`${branch}\`
**Commit:** \`${commit.substring(0, 7)}\`
**Triggered by:** ${actor}
**Links:**
- [View Workflow](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})
- [View Commit](${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/commit/${commit})
`;
const response = await fetch('https://app.notifer.io/ci-pipeline', {
method: 'POST',
headers: {
'X-Title': `${workflow}: ${status}`,
'X-Priority': priority.toString(),
'X-Tags': `ci,${status},${branch}`,
'X-Markdown': 'true'
},
body: message
});
if (!response.ok) {
console.error('Failed to send notification');
}
}
// Usage in workflow
const status = process.argv[2]; // 'success' or 'failure'
await notifyDeployment(status, {});
TypeScript Client (Strongly Typed)¶
interface PublishOptions {
title?: string;
priority?: 1 | 2 | 3 | 4 | 5;
tags?: string[];
markdown?: boolean;
}
interface Message {
id: string;
topic: string;
message: string;
title?: string;
priority: number;
tags: string[];
created_at: string;
}
class NotiferClient {
private baseUrl: string;
private apiKey?: string;
constructor(apiKey?: string, baseUrl = 'https://app.notifer.io') {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
async publish(
topic: string,
message: string,
options: PublishOptions = {}
): Promise<Message> {
const {
title,
priority = 3,
tags,
markdown = false
} = options;
const headers: Record<string, string> = {
'X-Priority': priority.toString()
};
if (this.apiKey) {
headers['Authorization'] = `Bearer ${this.apiKey}`;
}
if (title) {
headers['X-Title'] = title;
}
if (tags && tags.length > 0) {
headers['X-Tags'] = tags.join(',');
}
if (markdown) {
headers['X-Markdown'] = 'true';
}
const response = await fetch(`${this.baseUrl}/${topic}`, {
method: 'POST',
headers,
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json() as Message;
}
}
// Usage with type safety
const client = new NotiferClient(process.env.NOTIFER_API_KEY);
const result: Message = await client.publish(
'alerts',
'Test message',
{
title: 'Test Alert',
priority: 4,
tags: ['test', 'demo'],
markdown: true
}
);
console.log(`Published message: ${result.id}`);
React Hook for Notifications¶
import { useState, useCallback } from 'react';
function useNotifer(apiKey) {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const publish = useCallback(async (topic, message, options = {}) => {
setLoading(true);
setError(null);
try {
const headers = {
'X-Priority': (options.priority || 3).toString()
};
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}
if (options.title) {
headers['X-Title'] = options.title;
}
if (options.tags) {
headers['X-Tags'] = options.tags.join(',');
}
if (options.markdown) {
headers['X-Markdown'] = 'true';
}
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers,
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (err) {
setError(err.message);
throw err;
} finally {
setLoading(false);
}
}, [apiKey]);
return { publish, loading, error };
}
// Usage in component
function NotificationButton() {
const { publish, loading, error } = useNotifer(process.env.REACT_APP_NOTIFER_KEY);
const handleClick = async () => {
try {
await publish('user-actions', 'Button clicked', {
title: 'User Interaction',
priority: 2,
tags: ['ui', 'interaction']
});
console.log('Notification sent!');
} catch (error) {
console.error('Failed to send notification:', error);
}
};
return (
<button onClick={handleClick} disabled={loading}>
{loading ? 'Sending...' : 'Send Notification'}
</button>
);
}
Error Handling¶
Basic Error Handling¶
async function safePublish(topic, message, options = {}) {
try {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: options.headers || {},
body: message,
signal: AbortSignal.timeout(5000) // 5 second timeout
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TimeoutError') {
console.error('Request timeout');
} else if (error.name === 'TypeError') {
console.error('Network error');
} else {
console.error('Error:', error.message);
}
return null;
}
}
Retry with Exponential Backoff¶
async function publishWithRetry(topic, message, options = {}, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: options.headers || {},
body: message,
signal: AbortSignal.timeout(5000)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (attempt === maxRetries - 1) {
// Last attempt failed
console.error(`Failed after ${maxRetries} attempts:`, error);
throw error;
}
// Calculate backoff delay: 2^attempt seconds
const delay = Math.pow(2, attempt) * 1000;
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
Promise-Based Queue¶
class NotificationQueue {
constructor(concurrency = 3) {
this.queue = [];
this.running = 0;
this.concurrency = concurrency;
}
async add(topic, message, options = {}) {
return new Promise((resolve, reject) => {
this.queue.push({ topic, message, options, resolve, reject });
this.process();
});
}
async process() {
if (this.running >= this.concurrency || this.queue.length === 0) {
return;
}
this.running++;
const { topic, message, options, resolve, reject } = this.queue.shift();
try {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: options.headers || {},
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
resolve(data);
} catch (error) {
reject(error);
} finally {
this.running--;
this.process();
}
}
}
// Usage
const queue = new NotificationQueue(5); // Max 5 concurrent requests
// Add multiple notifications
const promises = [
queue.add('topic1', 'Message 1'),
queue.add('topic2', 'Message 2'),
queue.add('topic3', 'Message 3')
];
const results = await Promise.all(promises);
Best Practices¶
1. Use Environment Variables¶
const topic = process.env.NOTIFER_TOPIC || 'default-topic';
const apiKey = process.env.NOTIFER_API_KEY;
async function publish(message, options = {}) {
const headers = { ...options.headers };
if (apiKey) {
headers['Authorization'] = `Bearer ${apiKey}`;
}
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers,
body: message
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
2. Create Singleton Instance¶
// notifer.js
class Notifer {
static instance = null;
static getInstance(apiKey) {
if (!Notifer.instance) {
Notifer.instance = new Notifer(apiKey);
}
return Notifer.instance;
}
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://app.notifer.io';
}
async publish(topic, message, options = {}) {
// ... implementation
}
}
export default Notifer.getInstance(process.env.NOTIFER_API_KEY);
// Usage in other files
import notifer from './notifer.js';
await notifer.publish('alerts', 'Test message');
3. Add Request Timeout¶
async function publishWithTimeout(topic, message, timeoutMs = 5000) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
const response = await fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
body: message,
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
} finally {
clearTimeout(timeout);
}
}
4. Batch Notifications¶
async function publishBatch(notifications) {
const promises = notifications.map(({ topic, message, options }) =>
fetch(`https://app.notifer.io/${topic}`, {
method: 'POST',
headers: options?.headers || {},
body: message
})
);
const results = await Promise.allSettled(promises);
return results.map((result, index) => ({
...notifications[index],
success: result.status === 'fulfilled',
error: result.status === 'rejected' ? result.reason : null
}));
}
// Usage
const results = await publishBatch([
{ topic: 'alerts', message: 'Alert 1' },
{ topic: 'logs', message: 'Log entry 1' },
{ topic: 'metrics', message: 'Metric data 1' }
]);
console.log(`Sent ${results.filter(r => r.success).length}/${results.length} notifications`);
Testing¶
Unit Tests (Jest)¶
import { jest } from '@jest/globals';
// Mock fetch
global.fetch = jest.fn();
describe('NotiferClient', () => {
beforeEach(() => {
fetch.mockClear();
});
test('publishes message successfully', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({
id: 'test-id',
topic: 'test-topic',
message: 'Test message'
})
});
const client = new NotiferClient('test-key');
const result = await client.publish('test-topic', 'Test message');
expect(result.id).toBe('test-id');
expect(fetch).toHaveBeenCalledTimes(1);
});
test('handles error correctly', async () => {
fetch.mockResolvedValueOnce({
ok: false,
status: 500
});
const client = new NotiferClient('test-key');
await expect(
client.publish('test-topic', 'Test message')
).rejects.toThrow('HTTP error! status: 500');
});
});
Next Steps¶
- Python Publishing - Publish from Python
- HTTP Publishing - Publish via cURL and HTTP
- API Reference - Complete API documentation
- SSE Guide - Subscribe to topics in JavaScript
Pro Tip: Use TypeScript for type safety and create a singleton client instance for cleaner code! 🚀