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:
- SEO Penalties: Search engines like Google penalize websites with broken links, pushing you down in rankings
- Lost Revenue: Each broken link in an e-commerce site could mean lost sales
- Poor User Experience: Dead links destroy trust and credibility
- 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:
-
Monitor Critical Links More Frequently: Set
hourlycheck frequency for high-value pages (checkout, pricing, sign-up) -
Use Groups to Organize Links: Group links by priority, department, or page type for easier management
-
Set Up Immediate Alerts: Configure webhooks to get instant notifications when a dead link is detected
-
Regular Batch Updates: Periodically scan your sitemap and register new URLs for monitoring
-
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!