jsmanifest logojsmanifest

Automate Link Checking in TypeScript with the DeadLinkRadar API

Automate Link Checking in TypeScript with the DeadLinkRadar API

Learn how to build a robust dead link detection system using TypeScript and the DeadLinkRadar REST API. Protect your SEO rankings, avoid money loss from broken links, and automate link monitoring to deliver a seamless user experience.

Broken links are silent killers of your online business. They hurt your SEO rankings, frustrate visitors, and ultimately lead to money loss. According to studies, broken links can reduce your search engine rankings by up to 15%, and visitors who encounter dead links are 88% less likely to return to your site.

In this tutorial, we'll build a complete dead link detection and monitoring system using TypeScript and the DeadLinkRadar REST API. This solution will help you automate link checking across your website, monitor file hosting services, and receive alerts before broken links cost you customers.

The Real Cost of Dead Links

Before diving into code, let's understand why link checking matters:

  1. SEO Penalties: Search engines like Google penalize websites with broken links, pushing you down in rankings
  2. Lost Revenue: Each broken link in an e-commerce site could mean lost sales
  3. Poor User Experience: Dead links destroy trust and credibility
  4. Wasted Ad Spend: If your paid traffic lands on broken pages, you're literally burning money

The solution? Automated dead link monitoring that catches problems before your users (or Google) do.

Setting Up the DeadLinkRadar API Client

Let's start by creating a type-safe TypeScript client for the DeadLinkRadar API. This will be our foundation for all link monitoring operations.

First, let's define our types:

// types/deadlinkradar.ts
 
export type LinkStatus = 'active' | 'dead' | 'unknown' | 'checking'
export type CheckFrequency = 'hourly' | 'daily' | 'weekly' | 'monthly'
 
export interface Link {
  id: string
  url: string
  status: LinkStatus
  group_id: string | null
  check_frequency: CheckFrequency
  last_checked_at: string | null
  created_at: string
  updated_at: string
}
 
export interface LinkHistory {
  id: string
  link_id: string
  status: LinkStatus
  checked_at: string
  response_time_ms: number | null
  error_message: string | null
}
 
export interface CreateLinkRequest {
  url: string
  group_id?: string
  check_frequency?: CheckFrequency
}
 
export interface BatchCreateLinksRequest {
  links: CreateLinkRequest[]
}
 
export interface UpdateLinkRequest {
  group_id?: string | null
  check_frequency?: CheckFrequency
}
 
export interface ListLinksParams {
  status?: LinkStatus
  service?: string
  group_id?: string
  search?: string
  page?: number
  per_page?: number
}
 
export interface PaginatedResponse<T> {
  data: T[]
  meta: {
    current_page: number
    per_page: number
    total: number
    total_pages: number
  }
}
 
export interface ApiResponse<T> {
  data: T
}
 
export interface ApiError {
  error: {
    code: string
    message: string
  }
}

Now, let's create the API client class:

// lib/deadlinkradar-client.ts
 
import type {
  Link,
  LinkHistory,
  CreateLinkRequest,
  BatchCreateLinksRequest,
  UpdateLinkRequest,
  ListLinksParams,
  PaginatedResponse,
  ApiResponse,
  ApiError,
} from './types/deadlinkradar'
 
export class DeadLinkRadarClient {
  private readonly baseUrl = 'https://deadlinkradar.com/api/v1'
  private readonly apiKey: string
 
  constructor(apiKey: string) {
    if (!apiKey) {
      throw new Error('API key is required for DeadLinkRadar client')
    }
    this.apiKey = apiKey
  }
 
  private async request<T>(
    endpoint: string,
    options: RequestInit = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`
 
    const response = await fetch(url, {
      ...options,
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    })
 
    if (!response.ok) {
      const error: ApiError = await response.json()
      throw new Error(error.error?.message || `HTTP ${response.status}`)
    }
 
    return response.json()
  }
 
  /**
   * List all monitored links with optional filtering
   */
  async listLinks(
    params: ListLinksParams = {}
  ): Promise<PaginatedResponse<Link>> {
    const searchParams = new URLSearchParams()
 
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined) {
        searchParams.set(key, String(value))
      }
    })
 
    const query = searchParams.toString()
    const endpoint = `/links${query ? `?${query}` : ''}`
 
    return this.request<PaginatedResponse<Link>>(endpoint)
  }
 
  /**
   * Create a new monitored link
   */
  async createLink(data: CreateLinkRequest): Promise<ApiResponse<Link>> {
    return this.request<ApiResponse<Link>>('/links', {
      method: 'POST',
      body: JSON.stringify(data),
    })
  }
 
  /**
   * Batch create multiple links (up to 1,000)
   */
  async batchCreateLinks(
    data: BatchCreateLinksRequest
  ): Promise<ApiResponse<{ created: number; failed: number }>> {
    if (data.links.length > 1000) {
      throw new Error('Batch create is limited to 1,000 links per request')
    }
 
    return this.request<ApiResponse<{ created: number; failed: number }>>(
      '/links/batch',
      {
        method: 'POST',
        body: JSON.stringify(data),
      }
    )
  }
 
  /**
   * Get details of a specific link
   */
  async getLink(id: string): Promise<ApiResponse<Link>> {
    return this.request<ApiResponse<Link>>(`/links/${id}`)
  }
 
  /**
   * Update a link's settings
   */
  async updateLink(
    id: string,
    data: UpdateLinkRequest
  ): Promise<ApiResponse<Link>> {
    return this.request<ApiResponse<Link>>(`/links/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    })
  }
 
  /**
   * Delete a monitored link
   */
  async deleteLink(id: string): Promise<void> {
    await this.request(`/links/${id}`, { method: 'DELETE' })
  }
 
  /**
   * Trigger an immediate status check
   */
  async checkLinkNow(
    id: string
  ): Promise<ApiResponse<{ id: string; status: string; message: string }>> {
    return this.request(`/links/${id}/check`, { method: 'POST' })
  }
 
  /**
   * Get check history for a link
   */
  async getLinkHistory(
    id: string,
    params: {
      start_date?: string
      end_date?: string
      page?: number
      per_page?: number
    } = {}
  ): Promise<PaginatedResponse<LinkHistory>> {
    const searchParams = new URLSearchParams()
 
    Object.entries(params).forEach(([key, value]) => {
      if (value !== undefined) {
        searchParams.set(key, String(value))
      }
    })
 
    const query = searchParams.toString()
    const endpoint = `/links/${id}/history${query ? `?${query}` : ''}`
 
    return this.request<PaginatedResponse<LinkHistory>>(endpoint)
  }
}

Real-World Use Case: E-commerce Product Link Monitor

Let's build a practical system that monitors product links in an e-commerce site. This is crucial because dead product links mean direct money loss from missed sales.

// services/product-link-monitor.ts
 
import { DeadLinkRadarClient } from '../lib/deadlinkradar-client'
import type { Link, LinkStatus } from '../types/deadlinkradar'
 
interface ProductLink {
  productId: string
  productName: string
  url: string
  linkId?: string
}
 
interface MonitoringReport {
  totalLinks: number
  activeLinks: number
  deadLinks: Link[]
  unknownLinks: Link[]
  potentialRevenueLoss: number
  recommendations: string[]
}
 
export class ProductLinkMonitor {
  private client: DeadLinkRadarClient
  private averageOrderValue: number
 
  constructor(apiKey: string, averageOrderValue: number = 50) {
    this.client = new DeadLinkRadarClient(apiKey)
    this.averageOrderValue = averageOrderValue
  }
 
  /**
   * Register product links for monitoring
   */
  async registerProductLinks(products: ProductLink[]): Promise<void> {
    console.log(`Registering ${products.length} product links for monitoring...`)
 
    // Use batch creation for efficiency
    const links = products.map((product) => ({
      url: product.url,
      check_frequency: 'daily' as const,
    }))
 
    // Split into chunks of 1000 (API limit)
    const chunks = this.chunkArray(links, 1000)
 
    for (const chunk of chunks) {
      const result = await this.client.batchCreateLinks({ links: chunk })
      console.log(
        `Created ${result.data.created} links, ${result.data.failed} failed`
      )
    }
  }
 
  /**
   * Generate a comprehensive dead link report
   */
  async generateReport(): Promise<MonitoringReport> {
    const deadLinks: Link[] = []
    const unknownLinks: Link[] = []
    let totalLinks = 0
    let activeLinks = 0
 
    // Fetch all links with pagination
    let page = 1
    let hasMore = true
 
    while (hasMore) {
      const response = await this.client.listLinks({
        page,
        per_page: 100,
      })
 
      for (const link of response.data) {
        totalLinks++
 
        if (link.status === 'active') {
          activeLinks++
        } else if (link.status === 'dead') {
          deadLinks.push(link)
        } else if (link.status === 'unknown') {
          unknownLinks.push(link)
        }
      }
 
      hasMore = page < response.meta.total_pages
      page++
    }
 
    // Calculate potential revenue loss
    // Assuming each dead link loses ~10 visitors/day with 2% conversion
    const dailyLostVisitors = deadLinks.length * 10
    const dailyLostConversions = dailyLostVisitors * 0.02
    const potentialRevenueLoss = dailyLostConversions * this.averageOrderValue * 30
 
    // Generate recommendations
    const recommendations = this.generateRecommendations(
      deadLinks,
      unknownLinks,
      totalLinks
    )
 
    return {
      totalLinks,
      activeLinks,
      deadLinks,
      unknownLinks,
      potentialRevenueLoss,
      recommendations,
    }
  }
 
  /**
   * Find and fix dead links automatically
   */
  async findDeadLinks(): Promise<Link[]> {
    const response = await this.client.listLinks({
      status: 'dead',
      per_page: 100,
    })
 
    return response.data
  }
 
  /**
   * Trigger immediate checks on suspicious links
   */
  async recheckSuspiciousLinks(): Promise<void> {
    const unknownLinks = await this.client.listLinks({
      status: 'unknown',
      per_page: 100,
    })
 
    console.log(`Rechecking ${unknownLinks.data.length} suspicious links...`)
 
    for (const link of unknownLinks.data) {
      await this.client.checkLinkNow(link.id)
      // Small delay to avoid rate limiting
      await this.delay(100)
    }
  }
 
  private generateRecommendations(
    deadLinks: Link[],
    unknownLinks: Link[],
    totalLinks: number
  ): string[] {
    const recommendations: string[] = []
    const deadPercentage = (deadLinks.length / totalLinks) * 100
 
    if (deadLinks.length > 0) {
      recommendations.push(
        `URGENT: ${deadLinks.length} dead links detected. Each broken link costs you potential customers.`
      )
    }
 
    if (deadPercentage > 5) {
      recommendations.push(
        `WARNING: ${deadPercentage.toFixed(1)}% of your links are dead. This significantly impacts SEO.`
      )
    }
 
    if (unknownLinks.length > 10) {
      recommendations.push(
        `${unknownLinks.length} links have unknown status. Consider triggering manual checks.`
      )
    }
 
    if (deadLinks.length === 0 && unknownLinks.length === 0) {
      recommendations.push(
        'All links are healthy! Keep monitoring to catch issues early.'
      )
    }
 
    return recommendations
  }
 
  private chunkArray<T>(array: T[], size: number): T[][] {
    const chunks: T[][] = []
    for (let i = 0; i < array.length; i += size) {
      chunks.push(array.slice(i, i + size))
    }
    return chunks
  }
 
  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms))
  }
}

Building a Link Health Dashboard

Now let's create a dashboard component that displays link checking results in real-time:

// services/link-health-dashboard.ts
 
import { DeadLinkRadarClient } from '../lib/deadlinkradar-client'
import type { Link, LinkHistory, LinkStatus } from '../types/deadlinkradar'
 
interface HealthMetrics {
  uptime: number
  averageResponseTime: number
  checksLast24Hours: number
  statusBreakdown: Record<LinkStatus, number>
}
 
interface LinkHealthReport {
  link: Link
  history: LinkHistory[]
  metrics: HealthMetrics
  trend: 'improving' | 'stable' | 'declining'
}
 
export class LinkHealthDashboard {
  private client: DeadLinkRadarClient
 
  constructor(apiKey: string) {
    this.client = new DeadLinkRadarClient(apiKey)
  }
 
  /**
   * Get comprehensive health report for a single link
   */
  async getLinkHealthReport(linkId: string): Promise<LinkHealthReport> {
    const [linkResponse, historyResponse] = await Promise.all([
      this.client.getLink(linkId),
      this.client.getLinkHistory(linkId, {
        start_date: this.getDateDaysAgo(30),
        end_date: new Date().toISOString(),
        per_page: 100,
      }),
    ])
 
    const link = linkResponse.data
    const history = historyResponse.data
 
    const metrics = this.calculateMetrics(history)
    const trend = this.analyzeTrend(history)
 
    return { link, history, metrics, trend }
  }
 
  /**
   * Get overview of all monitored links
   */
  async getDashboardOverview(): Promise<{
    total: number
    healthy: number
    problematic: number
    critical: Link[]
    recentlyDead: Link[]
  }> {
    const [allLinks, deadLinks] = await Promise.all([
      this.client.listLinks({ per_page: 100 }),
      this.client.listLinks({ status: 'dead', per_page: 50 }),
    ])
 
    const total = allLinks.meta.total
    const healthy = allLinks.data.filter((l) => l.status === 'active').length
    const problematic = allLinks.data.filter(
      (l) => l.status === 'dead' || l.status === 'unknown'
    ).length
 
    // Find recently dead links (within last 24 hours)
    const recentlyDead = deadLinks.data.filter((link) => {
      if (!link.last_checked_at) return false
      const lastCheck = new Date(link.last_checked_at)
      const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
      return lastCheck > dayAgo
    })
 
    return {
      total,
      healthy,
      problematic,
      critical: deadLinks.data.slice(0, 10),
      recentlyDead,
    }
  }
 
  /**
   * Export dead link report as CSV
   */
  async exportDeadLinksCSV(): Promise<string> {
    const deadLinks = await this.client.listLinks({
      status: 'dead',
      per_page: 100,
    })
 
    const headers = ['URL', 'Last Checked', 'Created At', 'Link ID']
    const rows = deadLinks.data.map((link) => [
      link.url,
      link.last_checked_at || 'Never',
      link.created_at,
      link.id,
    ])
 
    return [headers.join(','), ...rows.map((row) => row.join(','))].join('\n')
  }
 
  private calculateMetrics(history: LinkHistory[]): HealthMetrics {
    const statusCounts: Record<LinkStatus, number> = {
      active: 0,
      dead: 0,
      unknown: 0,
      checking: 0,
    }
 
    let totalResponseTime = 0
    let responseTimeCount = 0
 
    for (const entry of history) {
      statusCounts[entry.status]++
 
      if (entry.response_time_ms) {
        totalResponseTime += entry.response_time_ms
        responseTimeCount++
      }
    }
 
    const uptime =
      history.length > 0
        ? (statusCounts.active / history.length) * 100
        : 0
 
    const averageResponseTime =
      responseTimeCount > 0 ? totalResponseTime / responseTimeCount : 0
 
    // Count checks in last 24 hours
    const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
    const checksLast24Hours = history.filter(
      (h) => new Date(h.checked_at) > dayAgo
    ).length
 
    return {
      uptime,
      averageResponseTime,
      checksLast24Hours,
      statusBreakdown: statusCounts,
    }
  }
 
  private analyzeTrend(
    history: LinkHistory[]
  ): 'improving' | 'stable' | 'declining' {
    if (history.length < 10) return 'stable'
 
    const recentHalf = history.slice(0, Math.floor(history.length / 2))
    const olderHalf = history.slice(Math.floor(history.length / 2))
 
    const recentActiveRate =
      recentHalf.filter((h) => h.status === 'active').length / recentHalf.length
    const olderActiveRate =
      olderHalf.filter((h) => h.status === 'active').length / olderHalf.length
 
    const difference = recentActiveRate - olderActiveRate
 
    if (difference > 0.1) return 'improving'
    if (difference < -0.1) return 'declining'
    return 'stable'
  }
 
  private getDateDaysAgo(days: number): string {
    const date = new Date()
    date.setDate(date.getDate() - days)
    return date.toISOString()
  }
}

Automating Link Monitoring with Scheduled Jobs

Here's how to set up automated link checking using a cron job or serverless function:

// jobs/scheduled-link-check.ts
 
import { DeadLinkRadarClient } from '../lib/deadlinkradar-client'
 
interface AlertConfig {
  webhookUrl?: string
  emailRecipients?: string[]
  slackChannel?: string
}
 
interface CheckResult {
  checked: number
  newDeadLinks: number
  recoveredLinks: number
  alerts: string[]
}
 
export class ScheduledLinkChecker {
  private client: DeadLinkRadarClient
  private alertConfig: AlertConfig
  private previousDeadLinkIds: Set<string> = new Set()
 
  constructor(apiKey: string, alertConfig: AlertConfig = {}) {
    this.client = new DeadLinkRadarClient(apiKey)
    this.alertConfig = alertConfig
  }
 
  /**
   * Run scheduled check and send alerts
   */
  async runScheduledCheck(): Promise<CheckResult> {
    console.log(`[${new Date().toISOString()}] Starting scheduled link check...`)
 
    // Get current dead links
    const currentDeadLinks = await this.client.listLinks({
      status: 'dead',
      per_page: 100,
    })
 
    const currentDeadIds = new Set(currentDeadLinks.data.map((l) => l.id))
 
    // Find newly dead links
    const newDeadLinks = currentDeadLinks.data.filter(
      (link) => !this.previousDeadLinkIds.has(link.id)
    )
 
    // Find recovered links
    const recoveredIds = [...this.previousDeadLinkIds].filter(
      (id) => !currentDeadIds.has(id)
    )
 
    const alerts: string[] = []
 
    // Generate alerts for new dead links
    if (newDeadLinks.length > 0) {
      const alertMessage = this.formatDeadLinkAlert(newDeadLinks)
      alerts.push(alertMessage)
      await this.sendAlerts(alertMessage, 'critical')
    }
 
    // Generate alerts for recovered links
    if (recoveredIds.length > 0) {
      const recoveryMessage = `${recoveredIds.length} link(s) have recovered and are now active.`
      alerts.push(recoveryMessage)
      await this.sendAlerts(recoveryMessage, 'info')
    }
 
    // Update previous state
    this.previousDeadLinkIds = currentDeadIds
 
    const result: CheckResult = {
      checked: currentDeadLinks.meta.total,
      newDeadLinks: newDeadLinks.length,
      recoveredLinks: recoveredIds.length,
      alerts,
    }
 
    console.log(`Check complete:`, result)
    return result
  }
 
  /**
   * Trigger immediate recheck of all links
   */
  async triggerFullRecheck(): Promise<void> {
    let page = 1
    let hasMore = true
 
    while (hasMore) {
      const links = await this.client.listLinks({ page, per_page: 50 })
 
      for (const link of links.data) {
        try {
          await this.client.checkLinkNow(link.id)
        } catch (error) {
          console.error(`Failed to trigger check for ${link.url}:`, error)
        }
        // Rate limiting
        await new Promise((resolve) => setTimeout(resolve, 200))
      }
 
      hasMore = page < links.meta.total_pages
      page++
    }
  }
 
  private formatDeadLinkAlert(deadLinks: { url: string; id: string }[]): string {
    const linkList = deadLinks
      .slice(0, 10)
      .map((l) => `  - ${l.url}`)
      .join('\n')
 
    return `ALERT: ${deadLinks.length} new dead link(s) detected!\n\n${linkList}${
      deadLinks.length > 10 ? `\n  ... and ${deadLinks.length - 10} more` : ''
    }\n\nDead links can cause money loss through decreased SEO rankings and poor user experience.`
  }
 
  private async sendAlerts(
    message: string,
    severity: 'critical' | 'warning' | 'info'
  ): Promise<void> {
    // Webhook notification
    if (this.alertConfig.webhookUrl) {
      try {
        await fetch(this.alertConfig.webhookUrl, {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ message, severity, timestamp: new Date().toISOString() }),
        })
      } catch (error) {
        console.error('Failed to send webhook alert:', error)
      }
    }
 
    // Add other notification channels (email, Slack) as needed
    console.log(`[ALERT - ${severity.toUpperCase()}] ${message}`)
  }
}
 
// Example usage in a serverless function or cron job
export async function handler(): Promise<void> {
  const apiKey = process.env.DEADLINKRADAR_API_KEY
 
  if (!apiKey) {
    throw new Error('DEADLINKRADAR_API_KEY environment variable is required')
  }
 
  const checker = new ScheduledLinkChecker(apiKey, {
    webhookUrl: process.env.ALERT_WEBHOOK_URL,
  })
 
  await checker.runScheduledCheck()
}

Putting It All Together

Here's a complete example showing how to use all the components:

// main.ts
 
import { DeadLinkRadarClient } from './lib/deadlinkradar-client'
import { ProductLinkMonitor } from './services/product-link-monitor'
import { LinkHealthDashboard } from './services/link-health-dashboard'
 
async function main() {
  const apiKey = process.env.DEADLINKRADAR_API_KEY!
 
  // Initialize services
  const monitor = new ProductLinkMonitor(apiKey, 75) // $75 average order value
  const dashboard = new LinkHealthDashboard(apiKey)
 
  // Register product links for monitoring
  const productLinks = [
    { productId: '1', productName: 'Widget Pro', url: 'https://example.com/products/widget-pro' },
    { productId: '2', productName: 'Gadget Plus', url: 'https://example.com/products/gadget-plus' },
    // ... more products
  ]
 
  await monitor.registerProductLinks(productLinks)
 
  // Generate health report
  const report = await monitor.generateReport()
 
  console.log('\n📊 Link Health Report')
  console.log('====================')
  console.log(`Total Links: ${report.totalLinks}`)
  console.log(`Active Links: ${report.activeLinks}`)
  console.log(`Dead Links: ${report.deadLinks.length}`)
  console.log(`Unknown Status: ${report.unknownLinks.length}`)
  console.log(`\n💰 Potential Monthly Revenue Loss: $${report.potentialRevenueLoss.toFixed(2)}`)
 
  console.log('\n📝 Recommendations:')
  report.recommendations.forEach((rec, i) => {
    console.log(`  ${i + 1}. ${rec}`)
  })
 
  // Get dashboard overview
  const overview = await dashboard.getDashboardOverview()
 
  if (overview.critical.length > 0) {
    console.log('\n🚨 Critical Dead Links Requiring Immediate Attention:')
    overview.critical.forEach((link) => {
      console.log(`  - ${link.url}`)
    })
  }
 
  // Export dead links for review
  const csv = await dashboard.exportDeadLinksCSV()
  console.log('\n📥 Dead links exported to CSV format')
}
 
main().catch(console.error)

Best Practices for Link Monitoring

To avoid money loss and maximize the effectiveness of your link checking system, follow these practices:

  1. Monitor Critical Links More Frequently: Set hourly check frequency for high-value pages (checkout, pricing, sign-up)

  2. Use Groups to Organize Links: Group links by priority, department, or page type for easier management

  3. Set Up Immediate Alerts: Configure webhooks to get instant notifications when a dead link is detected

  4. Regular Batch Updates: Periodically scan your sitemap and register new URLs for monitoring

  5. Review History Trends: Use historical data to identify patterns (hosting issues, expiring content)

Conclusion

Broken links are more than just a technical inconvenience; they're a direct threat to your revenue and reputation. By implementing automated link checking with the DeadLinkRadar API, you can:

  • Catch dead links before they hurt your SEO
  • Avoid money loss from broken e-commerce links
  • Maintain user trust with a seamless browsing experience
  • Save hours of manual link verification

The TypeScript implementation we built provides a type-safe, scalable foundation for monitoring hundreds or thousands of links across your properties. Start small, monitor your most critical pages, and expand from there.

Remember: in the world of web development, prevention is always cheaper than cure. A few minutes setting up link checking automation today could save you thousands in lost revenue tomorrow.

Happy monitoring!