Skip to content

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

npm install node-fetch
import fetch from 'node-fetch';

Official SDK (Coming Soon)

npm install @notifer/client

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

// .env file
NOTIFER_TOPIC=production-alerts
NOTIFER_API_KEY=your-api-key-here
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


Pro Tip: Use TypeScript for type safety and create a singleton client instance for cleaner code! 🚀